@adsim/wordpress-mcp-server 4.6.0 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +18 -0
- package/README.md +867 -499
- package/companion/mcp-diagnostics.php +1184 -0
- package/dxt/manifest.json +715 -98
- package/index.js +166 -4786
- package/package.json +14 -6
- package/src/data/plugin-performance-data.json +59 -0
- package/src/plugins/adapters/acf/acfAdapter.js +55 -3
- package/src/shared/api.js +79 -0
- package/src/shared/audit.js +39 -0
- package/src/shared/context.js +15 -0
- package/src/shared/governance.js +98 -0
- package/src/shared/utils.js +148 -0
- package/src/tools/comments.js +50 -0
- package/src/tools/content.js +395 -0
- package/src/tools/core.js +114 -0
- package/src/tools/editorial.js +634 -0
- package/src/tools/fse.js +370 -0
- package/src/tools/health.js +160 -0
- package/src/tools/index.js +96 -0
- package/src/tools/intelligence.js +2082 -0
- package/src/tools/links.js +118 -0
- package/src/tools/media.js +71 -0
- package/src/tools/performance.js +219 -0
- package/src/tools/plugins.js +368 -0
- package/src/tools/schema.js +417 -0
- package/src/tools/security.js +590 -0
- package/src/tools/seo.js +1633 -0
- package/src/tools/taxonomy.js +115 -0
- package/src/tools/users.js +188 -0
- package/src/tools/woocommerce.js +1008 -0
- package/src/tools/workflow.js +409 -0
- package/src/transport/http.js +39 -0
- package/tests/unit/helpers/pagination.test.js +43 -0
- package/tests/unit/plugins/acf/acfAdapter.test.js +43 -5
- package/tests/unit/tools/bulkUpdate.test.js +188 -0
- package/tests/unit/tools/diagnostics.test.js +397 -0
- package/tests/unit/tools/dynamicFiltering.test.js +100 -8
- package/tests/unit/tools/editorialIntelligence.test.js +817 -0
- package/tests/unit/tools/fse.test.js +548 -0
- package/tests/unit/tools/multilingual.test.js +653 -0
- package/tests/unit/tools/performance.test.js +351 -0
- package/tests/unit/tools/postMeta.test.js +105 -0
- package/tests/unit/tools/runWorkflow.test.js +150 -0
- package/tests/unit/tools/schema.test.js +477 -0
- package/tests/unit/tools/security.test.js +695 -0
- package/tests/unit/tools/site.test.js +1 -1
- package/tests/unit/tools/users.crud.test.js +399 -0
- package/tests/unit/tools/validateBlocks.test.js +186 -0
- package/tests/unit/tools/visualStaging.test.js +271 -0
- package/tests/unit/tools/woocommerce.advanced.test.js +679 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('node-fetch', () => ({ default: vi.fn() }));
|
|
4
|
+
|
|
5
|
+
import fetch from 'node-fetch';
|
|
6
|
+
import { handleToolCall } from '../../../index.js';
|
|
7
|
+
import { mockSuccess, mockError, getAuditLogs, makeRequest, parseResult } from '../../helpers/mockWpRequest.js';
|
|
8
|
+
|
|
9
|
+
function call(name, args = {}) {
|
|
10
|
+
return handleToolCall(makeRequest(name, args));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ════════════════════════════════════════════════════════════
|
|
14
|
+
// FSE: TEMPLATES
|
|
15
|
+
// ════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
describe('wp_list_templates', () => {
|
|
18
|
+
let consoleSpy;
|
|
19
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
20
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
21
|
+
|
|
22
|
+
it('returns formatted template list on success', async () => {
|
|
23
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
24
|
+
{ id: 'theme//index', slug: 'index', title: { rendered: 'Index' }, description: 'Main template', status: 'publish', type: 'wp_template', theme: 'twentytwentyfour', has_theme_file: true }
|
|
25
|
+
]));
|
|
26
|
+
const result = await call('wp_list_templates');
|
|
27
|
+
const data = parseResult(result);
|
|
28
|
+
expect(data.total).toBe(1);
|
|
29
|
+
expect(data.templates[0].id).toBe('theme//index');
|
|
30
|
+
expect(data.templates[0].slug).toBe('index');
|
|
31
|
+
expect(data.templates[0].has_theme_file).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('logs audit entry', async () => {
|
|
35
|
+
fetch.mockResolvedValue(mockSuccess([]));
|
|
36
|
+
await call('wp_list_templates');
|
|
37
|
+
const logs = getAuditLogs();
|
|
38
|
+
const entry = logs.find(l => l.tool === 'wp_list_templates');
|
|
39
|
+
expect(entry).toBeDefined();
|
|
40
|
+
expect(entry.status).toBe('success');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('wp_get_template', () => {
|
|
45
|
+
let consoleSpy;
|
|
46
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
47
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
48
|
+
|
|
49
|
+
it('returns full template data', async () => {
|
|
50
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
51
|
+
id: 'theme//single', slug: 'single', title: { rendered: 'Single' }, content: { raw: '<!-- wp:post-content /-->' },
|
|
52
|
+
description: 'Single post', status: 'publish', type: 'wp_template', theme: 'twentytwentyfour', has_theme_file: true, modified: '2024-01-01'
|
|
53
|
+
}));
|
|
54
|
+
const result = await call('wp_get_template', { id: 'theme//single' });
|
|
55
|
+
const data = parseResult(result);
|
|
56
|
+
expect(data.id).toBe('theme//single');
|
|
57
|
+
expect(data.content).toBe('<!-- wp:post-content /-->');
|
|
58
|
+
expect(data.title).toBe('Single');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns error on 404', async () => {
|
|
62
|
+
fetch.mockResolvedValue(mockError(404));
|
|
63
|
+
const result = await call('wp_get_template', { id: 'nonexistent//template' });
|
|
64
|
+
expect(result.isError).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('wp_create_template', () => {
|
|
69
|
+
let consoleSpy;
|
|
70
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
71
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
72
|
+
|
|
73
|
+
it('creates a template', async () => {
|
|
74
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
75
|
+
id: 'theme//custom', slug: 'custom', title: { rendered: 'Custom' }, status: 'publish'
|
|
76
|
+
}));
|
|
77
|
+
const result = await call('wp_create_template', { slug: 'custom', title: 'Custom', content: '<!-- wp:paragraph --><p>Hello</p><!-- /wp:paragraph -->' });
|
|
78
|
+
const data = parseResult(result);
|
|
79
|
+
expect(data.success).toBe(true);
|
|
80
|
+
expect(data.template.slug).toBe('custom');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('is blocked by WP_READ_ONLY', async () => {
|
|
84
|
+
process.env.WP_READ_ONLY = 'true';
|
|
85
|
+
try {
|
|
86
|
+
const result = await call('wp_create_template', { slug: 'test' });
|
|
87
|
+
expect(result.isError).toBe(true);
|
|
88
|
+
expect(result.content[0].text).toContain('READ-ONLY');
|
|
89
|
+
} finally {
|
|
90
|
+
delete process.env.WP_READ_ONLY;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('wp_update_template', () => {
|
|
96
|
+
let consoleSpy;
|
|
97
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
98
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
99
|
+
|
|
100
|
+
it('updates a template', async () => {
|
|
101
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
102
|
+
id: 'theme//index', slug: 'index', title: { rendered: 'Updated Index' }, status: 'publish'
|
|
103
|
+
}));
|
|
104
|
+
const result = await call('wp_update_template', { id: 'theme//index', title: 'Updated Index' });
|
|
105
|
+
const data = parseResult(result);
|
|
106
|
+
expect(data.success).toBe(true);
|
|
107
|
+
expect(data.template.title).toBe('Updated Index');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('wp_delete_template', () => {
|
|
112
|
+
let consoleSpy;
|
|
113
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
114
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
115
|
+
|
|
116
|
+
it('deletes a template', async () => {
|
|
117
|
+
fetch.mockResolvedValue(mockSuccess({ deleted: true }));
|
|
118
|
+
const result = await call('wp_delete_template', { id: 'theme//custom' });
|
|
119
|
+
const data = parseResult(result);
|
|
120
|
+
expect(data.success).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('is blocked by WP_DISABLE_DELETE', async () => {
|
|
124
|
+
process.env.WP_DISABLE_DELETE = 'true';
|
|
125
|
+
try {
|
|
126
|
+
const result = await call('wp_delete_template', { id: 'theme//custom' });
|
|
127
|
+
expect(result.isError).toBe(true);
|
|
128
|
+
expect(result.content[0].text).toContain('DISABLE_DELETE');
|
|
129
|
+
} finally {
|
|
130
|
+
delete process.env.WP_DISABLE_DELETE;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ════════════════════════════════════════════════════════════
|
|
136
|
+
// FSE: TEMPLATE PARTS
|
|
137
|
+
// ════════════════════════════════════════════════════════════
|
|
138
|
+
|
|
139
|
+
describe('wp_list_template_parts', () => {
|
|
140
|
+
let consoleSpy;
|
|
141
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
142
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
143
|
+
|
|
144
|
+
it('returns template parts with area', async () => {
|
|
145
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
146
|
+
{ id: 'theme//header', slug: 'header', title: { rendered: 'Header' }, area: 'header', description: 'Site header', status: 'publish', theme: 'twentytwentyfour', has_theme_file: true }
|
|
147
|
+
]));
|
|
148
|
+
const result = await call('wp_list_template_parts', { area: 'header' });
|
|
149
|
+
const data = parseResult(result);
|
|
150
|
+
expect(data.total).toBe(1);
|
|
151
|
+
expect(data.template_parts[0].area).toBe('header');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('wp_get_template_part', () => {
|
|
156
|
+
let consoleSpy;
|
|
157
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
158
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
159
|
+
|
|
160
|
+
it('returns full template part data', async () => {
|
|
161
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
162
|
+
id: 'theme//footer', slug: 'footer', title: { rendered: 'Footer' }, content: { raw: '<!-- wp:paragraph --><p>Footer</p><!-- /wp:paragraph -->' },
|
|
163
|
+
area: 'footer', description: 'Site footer', status: 'publish', theme: 'twentytwentyfour', has_theme_file: true, modified: '2024-01-01'
|
|
164
|
+
}));
|
|
165
|
+
const result = await call('wp_get_template_part', { id: 'theme//footer' });
|
|
166
|
+
const data = parseResult(result);
|
|
167
|
+
expect(data.id).toBe('theme//footer');
|
|
168
|
+
expect(data.area).toBe('footer');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('wp_create_template_part', () => {
|
|
173
|
+
let consoleSpy;
|
|
174
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
175
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
176
|
+
|
|
177
|
+
it('creates a template part', async () => {
|
|
178
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
179
|
+
id: 'theme//sidebar', slug: 'sidebar', title: { rendered: 'Sidebar' }, area: 'general', status: 'publish'
|
|
180
|
+
}));
|
|
181
|
+
const result = await call('wp_create_template_part', { slug: 'sidebar', title: 'Sidebar', area: 'general' });
|
|
182
|
+
const data = parseResult(result);
|
|
183
|
+
expect(data.success).toBe(true);
|
|
184
|
+
expect(data.template_part.area).toBe('general');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('wp_update_template_part', () => {
|
|
189
|
+
let consoleSpy;
|
|
190
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
191
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
192
|
+
|
|
193
|
+
it('updates a template part', async () => {
|
|
194
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
195
|
+
id: 'theme//header', slug: 'header', title: { rendered: 'Updated Header' }, area: 'header', status: 'publish'
|
|
196
|
+
}));
|
|
197
|
+
const result = await call('wp_update_template_part', { id: 'theme//header', title: 'Updated Header' });
|
|
198
|
+
const data = parseResult(result);
|
|
199
|
+
expect(data.success).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('wp_delete_template_part', () => {
|
|
204
|
+
let consoleSpy;
|
|
205
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
206
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
207
|
+
|
|
208
|
+
it('deletes a template part', async () => {
|
|
209
|
+
fetch.mockResolvedValue(mockSuccess({ deleted: true }));
|
|
210
|
+
const result = await call('wp_delete_template_part', { id: 'theme//custom-header' });
|
|
211
|
+
const data = parseResult(result);
|
|
212
|
+
expect(data.success).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ════════════════════════════════════════════════════════════
|
|
217
|
+
// FSE: GLOBAL STYLES
|
|
218
|
+
// ════════════════════════════════════════════════════════════
|
|
219
|
+
|
|
220
|
+
describe('wp_get_global_styles', () => {
|
|
221
|
+
let consoleSpy;
|
|
222
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
223
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
224
|
+
|
|
225
|
+
it('returns global styles data', async () => {
|
|
226
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
227
|
+
id: 42, title: { rendered: 'Custom Styles' },
|
|
228
|
+
styles: { color: { background: '#ffffff' }, typography: { fontSize: '16px' } },
|
|
229
|
+
settings: { color: { palette: [{ name: 'White', slug: 'white', color: '#ffffff' }] } },
|
|
230
|
+
modified: '2024-01-01'
|
|
231
|
+
}));
|
|
232
|
+
const result = await call('wp_get_global_styles', { id: 42 });
|
|
233
|
+
const data = parseResult(result);
|
|
234
|
+
expect(data.id).toBe(42);
|
|
235
|
+
expect(data.styles.color.background).toBe('#ffffff');
|
|
236
|
+
expect(data.settings.color.palette).toHaveLength(1);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('wp_update_global_styles', () => {
|
|
241
|
+
let consoleSpy;
|
|
242
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
243
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
244
|
+
|
|
245
|
+
it('updates global styles', async () => {
|
|
246
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
247
|
+
id: 42, title: { rendered: 'Custom Styles' },
|
|
248
|
+
styles: { color: { background: '#000000' } },
|
|
249
|
+
settings: {}
|
|
250
|
+
}));
|
|
251
|
+
const result = await call('wp_update_global_styles', { id: 42, styles: { color: { background: '#000000' } } });
|
|
252
|
+
const data = parseResult(result);
|
|
253
|
+
expect(data.success).toBe(true);
|
|
254
|
+
expect(data.global_styles.styles.color.background).toBe('#000000');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('is blocked by WP_READ_ONLY', async () => {
|
|
258
|
+
process.env.WP_READ_ONLY = 'true';
|
|
259
|
+
try {
|
|
260
|
+
const result = await call('wp_update_global_styles', { id: 42, styles: {} });
|
|
261
|
+
expect(result.isError).toBe(true);
|
|
262
|
+
expect(result.content[0].text).toContain('READ-ONLY');
|
|
263
|
+
} finally {
|
|
264
|
+
delete process.env.WP_READ_ONLY;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('wp_get_global_styles_variations', () => {
|
|
270
|
+
let consoleSpy;
|
|
271
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
272
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
273
|
+
|
|
274
|
+
it('returns style variations for a theme', async () => {
|
|
275
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
276
|
+
{ title: 'Dark', settings: { color: { palette: [] } }, styles: { color: { background: '#1a1a1a' } } },
|
|
277
|
+
{ title: 'Light', settings: {}, styles: { color: { background: '#ffffff' } } }
|
|
278
|
+
]));
|
|
279
|
+
const result = await call('wp_get_global_styles_variations', { stylesheet: 'twentytwentyfour' });
|
|
280
|
+
const data = parseResult(result);
|
|
281
|
+
expect(data.stylesheet).toBe('twentytwentyfour');
|
|
282
|
+
expect(data.total).toBe(2);
|
|
283
|
+
expect(data.variations).toHaveLength(2);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ════════════════════════════════════════════════════════════
|
|
288
|
+
// FSE: BLOCK PATTERNS
|
|
289
|
+
// ════════════════════════════════════════════════════════════
|
|
290
|
+
|
|
291
|
+
describe('wp_list_block_patterns', () => {
|
|
292
|
+
let consoleSpy;
|
|
293
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
294
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
295
|
+
|
|
296
|
+
it('returns block patterns list', async () => {
|
|
297
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
298
|
+
{ name: 'core/query-standard-posts', title: 'Standard Posts', description: 'A list of posts', categories: ['query'], keywords: ['posts'], blockTypes: ['core/query'], content: '<!-- wp:query --><!-- /wp:query -->' }
|
|
299
|
+
]));
|
|
300
|
+
const result = await call('wp_list_block_patterns');
|
|
301
|
+
const data = parseResult(result);
|
|
302
|
+
expect(data.total).toBe(1);
|
|
303
|
+
expect(data.patterns[0].name).toBe('core/query-standard-posts');
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('wp_get_block_pattern', () => {
|
|
308
|
+
let consoleSpy;
|
|
309
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
310
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
311
|
+
|
|
312
|
+
it('returns a single pattern by name', async () => {
|
|
313
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
314
|
+
{ name: 'core/query-standard-posts', title: 'Standard Posts', description: 'A list', content: '<!-- wp:query -->', categories: ['query'], keywords: ['posts'], blockTypes: ['core/query'] },
|
|
315
|
+
{ name: 'core/social-links', title: 'Social Links', description: 'Social', content: '<!-- wp:social-links -->', categories: ['social'], keywords: [], blockTypes: [] }
|
|
316
|
+
]));
|
|
317
|
+
const result = await call('wp_get_block_pattern', { name: 'core/social-links' });
|
|
318
|
+
const data = parseResult(result);
|
|
319
|
+
expect(data.name).toBe('core/social-links');
|
|
320
|
+
expect(data.title).toBe('Social Links');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('returns error when pattern not found', async () => {
|
|
324
|
+
fetch.mockResolvedValue(mockSuccess([]));
|
|
325
|
+
const result = await call('wp_get_block_pattern', { name: 'nonexistent/pattern' });
|
|
326
|
+
expect(result.isError).toBe(true);
|
|
327
|
+
expect(result.content[0].text).toContain('not found');
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('wp_create_block_pattern', () => {
|
|
332
|
+
let consoleSpy;
|
|
333
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
334
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
335
|
+
|
|
336
|
+
it('creates a block pattern', async () => {
|
|
337
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
338
|
+
id: 99, name: 'custom/hero', title: { rendered: 'Hero Section' }
|
|
339
|
+
}));
|
|
340
|
+
const result = await call('wp_create_block_pattern', { title: 'Hero Section', content: '<!-- wp:cover -->', name: 'custom/hero' });
|
|
341
|
+
const data = parseResult(result);
|
|
342
|
+
expect(data.success).toBe(true);
|
|
343
|
+
expect(data.pattern.name).toBe('custom/hero');
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('wp_delete_block_pattern', () => {
|
|
348
|
+
let consoleSpy;
|
|
349
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
350
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
351
|
+
|
|
352
|
+
it('deletes a block pattern', async () => {
|
|
353
|
+
fetch.mockResolvedValue(mockSuccess({ deleted: true }));
|
|
354
|
+
const result = await call('wp_delete_block_pattern', { id: 99 });
|
|
355
|
+
const data = parseResult(result);
|
|
356
|
+
expect(data.success).toBe(true);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ════════════════════════════════════════════════════════════
|
|
361
|
+
// FSE: NAVIGATION MENUS
|
|
362
|
+
// ════════════════════════════════════════════════════════════
|
|
363
|
+
|
|
364
|
+
describe('wp_list_navigation_menus', () => {
|
|
365
|
+
let consoleSpy;
|
|
366
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
367
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
368
|
+
|
|
369
|
+
it('returns navigation menus', async () => {
|
|
370
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
371
|
+
{ id: 1, title: { rendered: 'Main Menu' }, status: 'publish', date: '2024-01-01', modified: '2024-01-01', slug: 'main-menu' },
|
|
372
|
+
{ id: 2, title: { rendered: 'Footer Menu' }, status: 'publish', date: '2024-01-01', modified: '2024-01-01', slug: 'footer-menu' }
|
|
373
|
+
]));
|
|
374
|
+
const result = await call('wp_list_navigation_menus');
|
|
375
|
+
const data = parseResult(result);
|
|
376
|
+
expect(data.total).toBe(2);
|
|
377
|
+
expect(data.navigation_menus[0].title).toBe('Main Menu');
|
|
378
|
+
expect(data.navigation_menus[1].title).toBe('Footer Menu');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('supports search parameter', async () => {
|
|
382
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
383
|
+
{ id: 1, title: { rendered: 'Main Menu' }, status: 'publish', date: '2024-01-01', modified: '2024-01-01', slug: 'main-menu' }
|
|
384
|
+
]));
|
|
385
|
+
await call('wp_list_navigation_menus', { search: 'Main' });
|
|
386
|
+
const [url] = fetch.mock.calls[0];
|
|
387
|
+
expect(url).toContain('search=Main');
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('wp_get_navigation_menu', () => {
|
|
392
|
+
let consoleSpy;
|
|
393
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
394
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
395
|
+
|
|
396
|
+
it('returns full navigation menu data', async () => {
|
|
397
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
398
|
+
id: 1, title: { rendered: 'Main Menu' }, content: { rendered: '<!-- wp:navigation-link /-->' },
|
|
399
|
+
status: 'publish', date: '2024-01-01', modified: '2024-01-01', slug: 'main-menu'
|
|
400
|
+
}));
|
|
401
|
+
const result = await call('wp_get_navigation_menu', { id: 1 });
|
|
402
|
+
const data = parseResult(result);
|
|
403
|
+
expect(data.id).toBe(1);
|
|
404
|
+
expect(data.content).toBe('<!-- wp:navigation-link /-->');
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe('wp_create_navigation_menu', () => {
|
|
409
|
+
let consoleSpy;
|
|
410
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
411
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
412
|
+
|
|
413
|
+
it('creates a navigation menu', async () => {
|
|
414
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
415
|
+
id: 3, title: { rendered: 'New Menu' }, status: 'publish'
|
|
416
|
+
}));
|
|
417
|
+
const result = await call('wp_create_navigation_menu', { title: 'New Menu', content: '<!-- wp:navigation-link {"label":"Home","url":"/"} /-->' });
|
|
418
|
+
const data = parseResult(result);
|
|
419
|
+
expect(data.success).toBe(true);
|
|
420
|
+
expect(data.navigation.title).toBe('New Menu');
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe('wp_update_navigation_menu', () => {
|
|
425
|
+
let consoleSpy;
|
|
426
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
427
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
428
|
+
|
|
429
|
+
it('updates a navigation menu', async () => {
|
|
430
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
431
|
+
id: 1, title: { rendered: 'Updated Menu' }, status: 'publish'
|
|
432
|
+
}));
|
|
433
|
+
const result = await call('wp_update_navigation_menu', { id: 1, title: 'Updated Menu' });
|
|
434
|
+
const data = parseResult(result);
|
|
435
|
+
expect(data.success).toBe(true);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe('wp_delete_navigation_menu', () => {
|
|
440
|
+
let consoleSpy;
|
|
441
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
442
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
443
|
+
|
|
444
|
+
it('deletes a navigation menu', async () => {
|
|
445
|
+
fetch.mockResolvedValue(mockSuccess({ deleted: true }));
|
|
446
|
+
const result = await call('wp_delete_navigation_menu', { id: 1 });
|
|
447
|
+
const data = parseResult(result);
|
|
448
|
+
expect(data.success).toBe(true);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// ════════════════════════════════════════════════════════════
|
|
453
|
+
// FSE: WIDGETS
|
|
454
|
+
// ════════════════════════════════════════════════════════════
|
|
455
|
+
|
|
456
|
+
describe('wp_list_widgets', () => {
|
|
457
|
+
let consoleSpy;
|
|
458
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
459
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
460
|
+
|
|
461
|
+
it('returns widgets list', async () => {
|
|
462
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
463
|
+
{ id: 'text-2', id_base: 'text', sidebar: 'sidebar-1', rendered: '<div class="textwidget">Hello</div>' }
|
|
464
|
+
]));
|
|
465
|
+
const result = await call('wp_list_widgets');
|
|
466
|
+
const data = parseResult(result);
|
|
467
|
+
expect(data.total).toBe(1);
|
|
468
|
+
expect(data.widgets[0].id).toBe('text-2');
|
|
469
|
+
expect(data.widgets[0].sidebar).toBe('sidebar-1');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('filters by sidebar', async () => {
|
|
473
|
+
fetch.mockResolvedValue(mockSuccess([]));
|
|
474
|
+
await call('wp_list_widgets', { sidebar: 'sidebar-1' });
|
|
475
|
+
const [url] = fetch.mock.calls[0];
|
|
476
|
+
expect(url).toContain('sidebar=sidebar-1');
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
describe('wp_get_widget', () => {
|
|
481
|
+
let consoleSpy;
|
|
482
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
483
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
484
|
+
|
|
485
|
+
it('returns full widget data', async () => {
|
|
486
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
487
|
+
id: 'text-2', id_base: 'text', sidebar: 'sidebar-1',
|
|
488
|
+
instance: { raw: { title: 'Hello', text: 'World' } },
|
|
489
|
+
rendered: '<div class="textwidget"><p>World</p></div>'
|
|
490
|
+
}));
|
|
491
|
+
const result = await call('wp_get_widget', { id: 'text-2' });
|
|
492
|
+
const data = parseResult(result);
|
|
493
|
+
expect(data.id).toBe('text-2');
|
|
494
|
+
expect(data.instance.raw.title).toBe('Hello');
|
|
495
|
+
expect(data.rendered).toContain('textwidget');
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
describe('wp_update_widget', () => {
|
|
500
|
+
let consoleSpy;
|
|
501
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
502
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
503
|
+
|
|
504
|
+
it('updates a widget', async () => {
|
|
505
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
506
|
+
id: 'text-2', id_base: 'text', sidebar: 'sidebar-2'
|
|
507
|
+
}));
|
|
508
|
+
const result = await call('wp_update_widget', { id: 'text-2', sidebar: 'sidebar-2' });
|
|
509
|
+
const data = parseResult(result);
|
|
510
|
+
expect(data.success).toBe(true);
|
|
511
|
+
expect(data.widget.sidebar).toBe('sidebar-2');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('is blocked by WP_READ_ONLY', async () => {
|
|
515
|
+
process.env.WP_READ_ONLY = 'true';
|
|
516
|
+
try {
|
|
517
|
+
const result = await call('wp_update_widget', { id: 'text-2' });
|
|
518
|
+
expect(result.isError).toBe(true);
|
|
519
|
+
expect(result.content[0].text).toContain('READ-ONLY');
|
|
520
|
+
} finally {
|
|
521
|
+
delete process.env.WP_READ_ONLY;
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
describe('wp_delete_widget', () => {
|
|
527
|
+
let consoleSpy;
|
|
528
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
529
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
530
|
+
|
|
531
|
+
it('deletes a widget', async () => {
|
|
532
|
+
fetch.mockResolvedValue(mockSuccess({ deleted: true }));
|
|
533
|
+
const result = await call('wp_delete_widget', { id: 'text-2' });
|
|
534
|
+
const data = parseResult(result);
|
|
535
|
+
expect(data.success).toBe(true);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('is blocked by WP_DISABLE_DELETE', async () => {
|
|
539
|
+
process.env.WP_DISABLE_DELETE = 'true';
|
|
540
|
+
try {
|
|
541
|
+
const result = await call('wp_delete_widget', { id: 'text-2' });
|
|
542
|
+
expect(result.isError).toBe(true);
|
|
543
|
+
expect(result.content[0].text).toContain('DISABLE_DELETE');
|
|
544
|
+
} finally {
|
|
545
|
+
delete process.env.WP_DISABLE_DELETE;
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
});
|