@wordpress/abilities 0.1.1-next.dc3f6d3c1.0
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/CHANGELOG.md +9 -0
- package/LICENSE.md +788 -0
- package/README.md +302 -0
- package/build/api.js +189 -0
- package/build/api.js.map +7 -0
- package/build/index.js +53 -0
- package/build/index.js.map +7 -0
- package/build/store/actions.js +183 -0
- package/build/store/actions.js.map +7 -0
- package/build/store/constants.js +58 -0
- package/build/store/constants.js.map +7 -0
- package/build/store/index.js +77 -0
- package/build/store/index.js.map +7 -0
- package/build/store/reducer.js +126 -0
- package/build/store/reducer.js.map +7 -0
- package/build/store/resolvers.js +107 -0
- package/build/store/resolvers.js.map +7 -0
- package/build/store/selectors.js +64 -0
- package/build/store/selectors.js.map +7 -0
- package/build/store/tests/actions.test.js +792 -0
- package/build/store/tests/actions.test.js.map +7 -0
- package/build/store/tests/reducer.test.js +743 -0
- package/build/store/tests/reducer.test.js.map +7 -0
- package/build/store/tests/resolvers.test.js +520 -0
- package/build/store/tests/resolvers.test.js.map +7 -0
- package/build/store/tests/selectors.test.js +349 -0
- package/build/store/tests/selectors.test.js.map +7 -0
- package/build/tests/api.test.js +546 -0
- package/build/tests/api.test.js.map +7 -0
- package/build/tests/validation.test.js +453 -0
- package/build/tests/validation.test.js.map +7 -0
- package/build/types.js +19 -0
- package/build/types.js.map +7 -0
- package/build/validation.js +144 -0
- package/build/validation.js.map +7 -0
- package/build-module/api.js +146 -0
- package/build-module/api.js.map +7 -0
- package/build-module/index.js +28 -0
- package/build-module/index.js.map +7 -0
- package/build-module/store/actions.js +161 -0
- package/build-module/store/actions.js.map +7 -0
- package/build-module/store/constants.js +24 -0
- package/build-module/store/constants.js.map +7 -0
- package/build-module/store/index.js +47 -0
- package/build-module/store/index.js.map +7 -0
- package/build-module/store/reducer.js +112 -0
- package/build-module/store/reducer.js.map +7 -0
- package/build-module/store/resolvers.js +84 -0
- package/build-module/store/resolvers.js.map +7 -0
- package/build-module/store/selectors.js +36 -0
- package/build-module/store/selectors.js.map +7 -0
- package/build-module/store/tests/actions.test.js +804 -0
- package/build-module/store/tests/actions.test.js.map +7 -0
- package/build-module/store/tests/reducer.test.js +726 -0
- package/build-module/store/tests/reducer.test.js.map +7 -0
- package/build-module/store/tests/resolvers.test.js +523 -0
- package/build-module/store/tests/resolvers.test.js.map +7 -0
- package/build-module/store/tests/selectors.test.js +352 -0
- package/build-module/store/tests/selectors.test.js.map +7 -0
- package/build-module/tests/api.test.js +530 -0
- package/build-module/tests/api.test.js.map +7 -0
- package/build-module/tests/validation.test.js +451 -0
- package/build-module/tests/validation.test.js.map +7 -0
- package/build-module/types.js +1 -0
- package/build-module/types.js.map +7 -0
- package/build-module/validation.js +109 -0
- package/build-module/validation.js.map +7 -0
- package/build-types/api.d.ts +138 -0
- package/build-types/api.d.ts.map +1 -0
- package/build-types/index.d.ts +41 -0
- package/build-types/index.d.ts.map +1 -0
- package/build-types/store/actions.d.ts +76 -0
- package/build-types/store/actions.d.ts.map +1 -0
- package/build-types/store/constants.d.ts +14 -0
- package/build-types/store/constants.d.ts.map +1 -0
- package/build-types/store/index.d.ts +7 -0
- package/build-types/store/index.d.ts.map +1 -0
- package/build-types/store/reducer.d.ts +22 -0
- package/build-types/store/reducer.d.ts.map +1 -0
- package/build-types/store/resolvers.d.ts +47 -0
- package/build-types/store/resolvers.d.ts.map +1 -0
- package/build-types/store/selectors.d.ts +37 -0
- package/build-types/store/selectors.d.ts.map +1 -0
- package/build-types/types.d.ts +163 -0
- package/build-types/types.d.ts.map +1 -0
- package/build-types/validation.d.ts +23 -0
- package/build-types/validation.d.ts.map +1 -0
- package/package.json +52 -0
- package/src/api.ts +339 -0
- package/src/index.ts +65 -0
- package/src/store/actions.ts +247 -0
- package/src/store/constants.ts +15 -0
- package/src/store/index.ts +55 -0
- package/src/store/reducer.ts +184 -0
- package/src/store/resolvers.ts +151 -0
- package/src/store/selectors.ts +82 -0
- package/src/store/tests/actions.test.ts +950 -0
- package/src/store/tests/reducer.test.ts +861 -0
- package/src/store/tests/resolvers.test.ts +642 -0
- package/src/store/tests/selectors.test.ts +412 -0
- package/src/tests/api.test.ts +655 -0
- package/src/tests/validation.test.ts +532 -0
- package/src/types.ts +190 -0
- package/src/validation.ts +207 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for API functions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WordPress dependencies
|
|
7
|
+
*/
|
|
8
|
+
import { dispatch, select, resolveSelect } from '@wordpress/data';
|
|
9
|
+
import apiFetch from '@wordpress/api-fetch';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal dependencies
|
|
13
|
+
*/
|
|
14
|
+
import {
|
|
15
|
+
getAbilities,
|
|
16
|
+
getAbility,
|
|
17
|
+
getAbilityCategories,
|
|
18
|
+
getAbilityCategory,
|
|
19
|
+
registerAbility,
|
|
20
|
+
unregisterAbility,
|
|
21
|
+
executeAbility,
|
|
22
|
+
} from '../api';
|
|
23
|
+
import { store } from '../store';
|
|
24
|
+
import type { Ability, AbilityCategory } from '../types';
|
|
25
|
+
|
|
26
|
+
// Mock WordPress dependencies
|
|
27
|
+
jest.mock( '@wordpress/data', () => ( {
|
|
28
|
+
dispatch: jest.fn(),
|
|
29
|
+
select: jest.fn(),
|
|
30
|
+
resolveSelect: jest.fn(),
|
|
31
|
+
} ) );
|
|
32
|
+
|
|
33
|
+
jest.mock( '@wordpress/api-fetch' );
|
|
34
|
+
|
|
35
|
+
jest.mock( '../store', () => ( {
|
|
36
|
+
store: 'abilities-api/store',
|
|
37
|
+
} ) );
|
|
38
|
+
|
|
39
|
+
describe( 'API functions', () => {
|
|
40
|
+
beforeEach( () => {
|
|
41
|
+
jest.clearAllMocks();
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
describe( 'getAbilities', () => {
|
|
45
|
+
it( 'should resolve and return all abilities from the store', async () => {
|
|
46
|
+
const mockAbilities: Ability[] = [
|
|
47
|
+
{
|
|
48
|
+
name: 'test/ability1',
|
|
49
|
+
label: 'Test Ability 1',
|
|
50
|
+
description: 'First test ability',
|
|
51
|
+
category: 'test-category',
|
|
52
|
+
input_schema: { type: 'object' },
|
|
53
|
+
output_schema: { type: 'object' },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'test/ability2',
|
|
57
|
+
label: 'Test Ability 2',
|
|
58
|
+
description: 'Second test ability',
|
|
59
|
+
category: 'test-category',
|
|
60
|
+
input_schema: { type: 'object' },
|
|
61
|
+
output_schema: { type: 'object' },
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const mockGetAbilities = jest
|
|
66
|
+
.fn()
|
|
67
|
+
.mockResolvedValue( mockAbilities );
|
|
68
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
69
|
+
getAbilities: mockGetAbilities,
|
|
70
|
+
} );
|
|
71
|
+
|
|
72
|
+
const result = await getAbilities();
|
|
73
|
+
|
|
74
|
+
expect( resolveSelect ).toHaveBeenCalledWith( store );
|
|
75
|
+
expect( mockGetAbilities ).toHaveBeenCalled();
|
|
76
|
+
expect( result ).toEqual( mockAbilities );
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
it( 'should pass category parameter to store when filtering', async () => {
|
|
80
|
+
const mockAbilities: Ability[] = [
|
|
81
|
+
{
|
|
82
|
+
name: 'test/ability1',
|
|
83
|
+
label: 'Test Ability 1',
|
|
84
|
+
description: 'First test ability',
|
|
85
|
+
category: 'data-retrieval',
|
|
86
|
+
input_schema: { type: 'object' },
|
|
87
|
+
output_schema: { type: 'object' },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'test/ability2',
|
|
91
|
+
label: 'Test Ability 2',
|
|
92
|
+
description: 'Second test ability',
|
|
93
|
+
category: 'data-retrieval',
|
|
94
|
+
input_schema: { type: 'object' },
|
|
95
|
+
output_schema: { type: 'object' },
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const mockGetAbilities = jest
|
|
100
|
+
.fn()
|
|
101
|
+
.mockResolvedValue( mockAbilities );
|
|
102
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
103
|
+
getAbilities: mockGetAbilities,
|
|
104
|
+
} );
|
|
105
|
+
|
|
106
|
+
const result = await getAbilities( { category: 'data-retrieval' } );
|
|
107
|
+
|
|
108
|
+
expect( resolveSelect ).toHaveBeenCalledWith( store );
|
|
109
|
+
expect( mockGetAbilities ).toHaveBeenCalledWith( {
|
|
110
|
+
category: 'data-retrieval',
|
|
111
|
+
} );
|
|
112
|
+
expect( result ).toEqual( mockAbilities );
|
|
113
|
+
} );
|
|
114
|
+
} );
|
|
115
|
+
|
|
116
|
+
describe( 'getAbility', () => {
|
|
117
|
+
it( 'should return a specific ability by name', async () => {
|
|
118
|
+
const mockAbility: Ability = {
|
|
119
|
+
name: 'test/ability',
|
|
120
|
+
label: 'Test Ability',
|
|
121
|
+
description: 'Test ability description',
|
|
122
|
+
category: 'test-category',
|
|
123
|
+
input_schema: { type: 'object' },
|
|
124
|
+
output_schema: { type: 'object' },
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const mockGetAbility = jest.fn().mockResolvedValue( mockAbility );
|
|
128
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
129
|
+
getAbility: mockGetAbility,
|
|
130
|
+
} );
|
|
131
|
+
|
|
132
|
+
const result = await getAbility( 'test/ability' );
|
|
133
|
+
|
|
134
|
+
expect( resolveSelect ).toHaveBeenCalledWith( store );
|
|
135
|
+
expect( mockGetAbility ).toHaveBeenCalledWith( 'test/ability' );
|
|
136
|
+
expect( result ).toEqual( mockAbility );
|
|
137
|
+
} );
|
|
138
|
+
|
|
139
|
+
it( 'should return null if ability not found', async () => {
|
|
140
|
+
const mockGetAbility = jest.fn().mockResolvedValue( null );
|
|
141
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
142
|
+
getAbility: mockGetAbility,
|
|
143
|
+
} );
|
|
144
|
+
|
|
145
|
+
const result = await getAbility( 'non-existent' );
|
|
146
|
+
|
|
147
|
+
expect( mockGetAbility ).toHaveBeenCalledWith( 'non-existent' );
|
|
148
|
+
expect( result ).toBeNull();
|
|
149
|
+
} );
|
|
150
|
+
} );
|
|
151
|
+
|
|
152
|
+
describe( 'registerAbility', () => {
|
|
153
|
+
it( 'should register a client-side ability with a callback', () => {
|
|
154
|
+
const mockRegisterAbility = jest.fn();
|
|
155
|
+
( dispatch as jest.Mock ).mockReturnValue( {
|
|
156
|
+
registerAbility: mockRegisterAbility,
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
// Mock select to return no existing ability
|
|
160
|
+
( select as jest.Mock ).mockReturnValue( {
|
|
161
|
+
getAbility: jest.fn().mockReturnValue( null ),
|
|
162
|
+
} );
|
|
163
|
+
|
|
164
|
+
const ability = {
|
|
165
|
+
name: 'test/client-ability',
|
|
166
|
+
label: 'Client Ability',
|
|
167
|
+
description: 'Test client ability',
|
|
168
|
+
category: 'test-category',
|
|
169
|
+
input_schema: { type: 'object' },
|
|
170
|
+
output_schema: { type: 'object' },
|
|
171
|
+
callback: jest.fn(),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
registerAbility( ability );
|
|
175
|
+
|
|
176
|
+
expect( dispatch ).toHaveBeenCalledWith( store );
|
|
177
|
+
expect( mockRegisterAbility ).toHaveBeenCalledWith( ability );
|
|
178
|
+
} );
|
|
179
|
+
} );
|
|
180
|
+
|
|
181
|
+
describe( 'unregisterAbility', () => {
|
|
182
|
+
it( 'should unregister an ability', () => {
|
|
183
|
+
const mockUnregisterAbility = jest.fn();
|
|
184
|
+
( dispatch as jest.Mock ).mockReturnValue( {
|
|
185
|
+
unregisterAbility: mockUnregisterAbility,
|
|
186
|
+
} );
|
|
187
|
+
|
|
188
|
+
unregisterAbility( 'test/ability' );
|
|
189
|
+
|
|
190
|
+
expect( dispatch ).toHaveBeenCalledWith( store );
|
|
191
|
+
expect( mockUnregisterAbility ).toHaveBeenCalledWith(
|
|
192
|
+
'test/ability'
|
|
193
|
+
);
|
|
194
|
+
} );
|
|
195
|
+
} );
|
|
196
|
+
|
|
197
|
+
describe( 'executeAbility', () => {
|
|
198
|
+
it( 'should execute a server-side ability via API', async () => {
|
|
199
|
+
const mockAbility: Ability = {
|
|
200
|
+
name: 'test/server-ability',
|
|
201
|
+
label: 'Server Ability',
|
|
202
|
+
description: 'Test server ability',
|
|
203
|
+
category: 'test-category',
|
|
204
|
+
input_schema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
message: { type: 'string' },
|
|
208
|
+
},
|
|
209
|
+
required: [ 'message' ],
|
|
210
|
+
},
|
|
211
|
+
output_schema: { type: 'object' },
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const mockGetAbility = jest.fn().mockResolvedValue( mockAbility );
|
|
215
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
216
|
+
getAbility: mockGetAbility,
|
|
217
|
+
} );
|
|
218
|
+
|
|
219
|
+
const mockResponse = { success: true, result: 'test' };
|
|
220
|
+
( apiFetch as unknown as jest.Mock ).mockResolvedValue(
|
|
221
|
+
mockResponse
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const input = { message: 'Hello' };
|
|
225
|
+
const result = await executeAbility( 'test/server-ability', input );
|
|
226
|
+
|
|
227
|
+
expect( mockGetAbility ).toHaveBeenCalledWith(
|
|
228
|
+
'test/server-ability'
|
|
229
|
+
);
|
|
230
|
+
expect( apiFetch ).toHaveBeenCalledWith( {
|
|
231
|
+
path: '/wp-abilities/v1/abilities/test/server-ability/run',
|
|
232
|
+
method: 'POST',
|
|
233
|
+
data: { input },
|
|
234
|
+
} );
|
|
235
|
+
expect( result ).toEqual( mockResponse );
|
|
236
|
+
} );
|
|
237
|
+
|
|
238
|
+
it( 'should execute a client-side ability locally', async () => {
|
|
239
|
+
const mockCallback = jest
|
|
240
|
+
.fn()
|
|
241
|
+
.mockResolvedValue( { success: true } );
|
|
242
|
+
const mockAbility: Ability = {
|
|
243
|
+
name: 'test/client-ability',
|
|
244
|
+
label: 'Client Ability',
|
|
245
|
+
description: 'Test client ability',
|
|
246
|
+
category: 'test-category',
|
|
247
|
+
input_schema: { type: 'object' },
|
|
248
|
+
output_schema: { type: 'object' },
|
|
249
|
+
callback: mockCallback,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const mockGetAbility = jest.fn().mockResolvedValue( mockAbility );
|
|
253
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
254
|
+
getAbility: mockGetAbility,
|
|
255
|
+
} );
|
|
256
|
+
|
|
257
|
+
const input = { test: 'data' };
|
|
258
|
+
const result = await executeAbility( 'test/client-ability', input );
|
|
259
|
+
|
|
260
|
+
expect( mockGetAbility ).toHaveBeenCalledWith(
|
|
261
|
+
'test/client-ability'
|
|
262
|
+
);
|
|
263
|
+
expect( mockCallback ).toHaveBeenCalledWith( input );
|
|
264
|
+
expect( apiFetch ).not.toHaveBeenCalled();
|
|
265
|
+
expect( result ).toEqual( { success: true } );
|
|
266
|
+
} );
|
|
267
|
+
|
|
268
|
+
it( 'should throw error if ability not found', async () => {
|
|
269
|
+
const mockGetAbility = jest.fn().mockResolvedValue( null );
|
|
270
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
271
|
+
getAbility: mockGetAbility,
|
|
272
|
+
} );
|
|
273
|
+
|
|
274
|
+
await expect(
|
|
275
|
+
executeAbility( 'non-existent', {} )
|
|
276
|
+
).rejects.toThrow( 'Ability not found: non-existent' );
|
|
277
|
+
} );
|
|
278
|
+
|
|
279
|
+
it( 'should validate input for client abilities', async () => {
|
|
280
|
+
const mockCallback = jest.fn();
|
|
281
|
+
const mockAbility: Ability = {
|
|
282
|
+
name: 'test/client-ability',
|
|
283
|
+
label: 'Client Ability',
|
|
284
|
+
description: 'Test client ability',
|
|
285
|
+
category: 'test-category',
|
|
286
|
+
input_schema: {
|
|
287
|
+
type: 'object',
|
|
288
|
+
properties: {
|
|
289
|
+
message: { type: 'string' },
|
|
290
|
+
},
|
|
291
|
+
required: [ 'message' ],
|
|
292
|
+
},
|
|
293
|
+
output_schema: { type: 'object' },
|
|
294
|
+
callback: mockCallback,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const mockGetAbility = jest.fn().mockResolvedValue( mockAbility );
|
|
298
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
299
|
+
getAbility: mockGetAbility,
|
|
300
|
+
} );
|
|
301
|
+
|
|
302
|
+
await expect(
|
|
303
|
+
executeAbility( 'test/client-ability', {} )
|
|
304
|
+
).rejects.toThrow( 'invalid input' );
|
|
305
|
+
} );
|
|
306
|
+
|
|
307
|
+
it( 'should execute a read-only ability via GET', async () => {
|
|
308
|
+
const mockAbility: Ability = {
|
|
309
|
+
name: 'test/read-only',
|
|
310
|
+
label: 'Read-only Ability',
|
|
311
|
+
description: 'Test read-only ability.',
|
|
312
|
+
category: 'test-category',
|
|
313
|
+
input_schema: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
properties: {
|
|
316
|
+
id: { type: 'string' },
|
|
317
|
+
format: { type: 'string' },
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
output_schema: { type: 'object' },
|
|
321
|
+
meta: {
|
|
322
|
+
annotations: { readonly: true },
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const mockGetAbility = jest.fn().mockResolvedValue( mockAbility );
|
|
327
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
328
|
+
getAbility: mockGetAbility,
|
|
329
|
+
} );
|
|
330
|
+
|
|
331
|
+
const mockResponse = { data: 'read-only data' };
|
|
332
|
+
( apiFetch as unknown as jest.Mock ).mockResolvedValue(
|
|
333
|
+
mockResponse
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const input = { id: '123', format: 'json' };
|
|
337
|
+
const result = await executeAbility( 'test/read-only', input );
|
|
338
|
+
|
|
339
|
+
expect( apiFetch ).toHaveBeenCalledWith( {
|
|
340
|
+
path: '/wp-abilities/v1/abilities/test/read-only/run?input%5Bid%5D=123&input%5Bformat%5D=json',
|
|
341
|
+
method: 'GET',
|
|
342
|
+
} );
|
|
343
|
+
expect( result ).toEqual( mockResponse );
|
|
344
|
+
} );
|
|
345
|
+
|
|
346
|
+
it( 'should execute a read-only ability with empty input', async () => {
|
|
347
|
+
const mockAbility: Ability = {
|
|
348
|
+
name: 'test/read-only',
|
|
349
|
+
label: 'Read-only Ability',
|
|
350
|
+
description: 'Test read-only ability.',
|
|
351
|
+
category: 'test-category',
|
|
352
|
+
input_schema: { type: 'object' },
|
|
353
|
+
output_schema: { type: 'object' },
|
|
354
|
+
meta: {
|
|
355
|
+
annotations: { readonly: true },
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const mockGetAbility = jest.fn().mockResolvedValue( mockAbility );
|
|
360
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
361
|
+
getAbility: mockGetAbility,
|
|
362
|
+
} );
|
|
363
|
+
|
|
364
|
+
const mockResponse = { data: 'read-only data' };
|
|
365
|
+
( apiFetch as unknown as jest.Mock ).mockResolvedValue(
|
|
366
|
+
mockResponse
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const result = await executeAbility( 'test/read-only', {} );
|
|
370
|
+
|
|
371
|
+
expect( apiFetch ).toHaveBeenCalledWith( {
|
|
372
|
+
path: '/wp-abilities/v1/abilities/test/read-only/run?',
|
|
373
|
+
method: 'GET',
|
|
374
|
+
} );
|
|
375
|
+
expect( result ).toEqual( mockResponse );
|
|
376
|
+
} );
|
|
377
|
+
|
|
378
|
+
it( 'should execute a destructive idempotent ability via DELETE', async () => {
|
|
379
|
+
const mockAbility: Ability = {
|
|
380
|
+
name: 'test/destructive',
|
|
381
|
+
label: 'Destructive Ability',
|
|
382
|
+
description: 'Test destructive idempotent ability.',
|
|
383
|
+
category: 'test-category',
|
|
384
|
+
input_schema: {
|
|
385
|
+
type: 'object',
|
|
386
|
+
properties: {
|
|
387
|
+
id: { type: 'string' },
|
|
388
|
+
format: { type: 'string' },
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
output_schema: { type: 'string' },
|
|
392
|
+
meta: {
|
|
393
|
+
annotations: {
|
|
394
|
+
destructive: true,
|
|
395
|
+
idempotent: true,
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const mockGetAbility = jest.fn().mockResolvedValue( mockAbility );
|
|
401
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
402
|
+
getAbility: mockGetAbility,
|
|
403
|
+
} );
|
|
404
|
+
|
|
405
|
+
const mockResponse = 'Item deleted successfully.';
|
|
406
|
+
( apiFetch as unknown as jest.Mock ).mockResolvedValue(
|
|
407
|
+
mockResponse
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const input = { id: '123', format: 'json' };
|
|
411
|
+
const result = await executeAbility( 'test/destructive', input );
|
|
412
|
+
|
|
413
|
+
expect( apiFetch ).toHaveBeenCalledWith( {
|
|
414
|
+
path: '/wp-abilities/v1/abilities/test/destructive/run?input%5Bid%5D=123&input%5Bformat%5D=json',
|
|
415
|
+
method: 'DELETE',
|
|
416
|
+
} );
|
|
417
|
+
expect( result ).toEqual( mockResponse );
|
|
418
|
+
} );
|
|
419
|
+
|
|
420
|
+
it( 'should handle errors in client ability execution', async () => {
|
|
421
|
+
const consoleErrorSpy = jest
|
|
422
|
+
.spyOn( console, 'error' )
|
|
423
|
+
.mockImplementation();
|
|
424
|
+
const executionError = new Error( 'Execution failed' );
|
|
425
|
+
const mockCallback = jest.fn().mockRejectedValue( executionError );
|
|
426
|
+
|
|
427
|
+
const mockAbility: Ability = {
|
|
428
|
+
name: 'test/client-ability',
|
|
429
|
+
label: 'Client Ability',
|
|
430
|
+
description: 'Test client ability',
|
|
431
|
+
category: 'test-category',
|
|
432
|
+
input_schema: { type: 'object' },
|
|
433
|
+
output_schema: { type: 'object' },
|
|
434
|
+
callback: mockCallback,
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const mockGetAbility = jest.fn().mockResolvedValue( mockAbility );
|
|
438
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
439
|
+
getAbility: mockGetAbility,
|
|
440
|
+
} );
|
|
441
|
+
|
|
442
|
+
await expect(
|
|
443
|
+
executeAbility( 'test/client-ability', {} )
|
|
444
|
+
).rejects.toThrow( 'Execution failed' );
|
|
445
|
+
|
|
446
|
+
expect( consoleErrorSpy ).toHaveBeenCalledWith(
|
|
447
|
+
'Error executing client ability test/client-ability:',
|
|
448
|
+
executionError
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
consoleErrorSpy.mockRestore();
|
|
452
|
+
} );
|
|
453
|
+
|
|
454
|
+
it( 'should handle errors in server ability execution', async () => {
|
|
455
|
+
const consoleErrorSpy = jest
|
|
456
|
+
.spyOn( console, 'error' )
|
|
457
|
+
.mockImplementation();
|
|
458
|
+
const apiError = new Error( 'API request failed' );
|
|
459
|
+
|
|
460
|
+
const mockAbility: Ability = {
|
|
461
|
+
name: 'test/server-ability',
|
|
462
|
+
label: 'Server Ability',
|
|
463
|
+
description: 'Test server ability',
|
|
464
|
+
category: 'test-category',
|
|
465
|
+
input_schema: { type: 'object' },
|
|
466
|
+
output_schema: { type: 'object' },
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const mockGetAbility = jest.fn().mockResolvedValue( mockAbility );
|
|
470
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
471
|
+
getAbility: mockGetAbility,
|
|
472
|
+
} );
|
|
473
|
+
|
|
474
|
+
( apiFetch as unknown as jest.Mock ).mockRejectedValue( apiError );
|
|
475
|
+
|
|
476
|
+
await expect(
|
|
477
|
+
executeAbility( 'test/server-ability', {} )
|
|
478
|
+
).rejects.toThrow( 'API request failed' );
|
|
479
|
+
|
|
480
|
+
expect( consoleErrorSpy ).toHaveBeenCalledWith(
|
|
481
|
+
'Error executing ability test/server-ability:',
|
|
482
|
+
apiError
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
consoleErrorSpy.mockRestore();
|
|
486
|
+
} );
|
|
487
|
+
|
|
488
|
+
it( 'should execute ability without callback as server ability', async () => {
|
|
489
|
+
const mockAbility: Ability = {
|
|
490
|
+
name: 'test/ability',
|
|
491
|
+
label: 'Test Ability',
|
|
492
|
+
description: 'Test ability without callback',
|
|
493
|
+
category: 'test-category',
|
|
494
|
+
input_schema: { type: 'object' },
|
|
495
|
+
output_schema: { type: 'object' },
|
|
496
|
+
// No callback - should execute as server ability
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const mockGetAbility = jest.fn().mockResolvedValue( mockAbility );
|
|
500
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
501
|
+
getAbility: mockGetAbility,
|
|
502
|
+
} );
|
|
503
|
+
|
|
504
|
+
const mockResponse = { success: true };
|
|
505
|
+
( apiFetch as unknown as jest.Mock ).mockResolvedValue(
|
|
506
|
+
mockResponse
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const result = await executeAbility( 'test/ability', {
|
|
510
|
+
data: 'test',
|
|
511
|
+
} );
|
|
512
|
+
|
|
513
|
+
expect( apiFetch ).toHaveBeenCalledWith( {
|
|
514
|
+
path: '/wp-abilities/v1/abilities/test/ability/run',
|
|
515
|
+
method: 'POST',
|
|
516
|
+
data: { input: { data: 'test' } },
|
|
517
|
+
} );
|
|
518
|
+
expect( result ).toEqual( mockResponse );
|
|
519
|
+
} );
|
|
520
|
+
|
|
521
|
+
it( 'should validate output for client abilities', async () => {
|
|
522
|
+
const mockCallback = jest
|
|
523
|
+
.fn()
|
|
524
|
+
.mockResolvedValue( { invalid: 'response' } );
|
|
525
|
+
const mockAbility: Ability = {
|
|
526
|
+
name: 'test/client-ability',
|
|
527
|
+
label: 'Client Ability',
|
|
528
|
+
description: 'Test client ability',
|
|
529
|
+
category: 'test-category',
|
|
530
|
+
input_schema: { type: 'object' },
|
|
531
|
+
output_schema: {
|
|
532
|
+
type: 'object',
|
|
533
|
+
properties: {
|
|
534
|
+
result: { type: 'string' },
|
|
535
|
+
},
|
|
536
|
+
required: [ 'result' ],
|
|
537
|
+
},
|
|
538
|
+
callback: mockCallback,
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const mockGetAbility = jest.fn().mockResolvedValue( mockAbility );
|
|
542
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
543
|
+
getAbility: mockGetAbility,
|
|
544
|
+
} );
|
|
545
|
+
|
|
546
|
+
await expect(
|
|
547
|
+
executeAbility( 'test/client-ability', {} )
|
|
548
|
+
).rejects.toThrow( 'invalid output' );
|
|
549
|
+
} );
|
|
550
|
+
} );
|
|
551
|
+
|
|
552
|
+
describe( 'getAbilityCategories', () => {
|
|
553
|
+
it( 'should resolve and return all categories from the store', async () => {
|
|
554
|
+
const mockCategories: AbilityCategory[] = [
|
|
555
|
+
{
|
|
556
|
+
slug: 'data-retrieval',
|
|
557
|
+
label: 'Data Retrieval',
|
|
558
|
+
description: 'Abilities that retrieve data',
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
slug: 'user-management',
|
|
562
|
+
label: 'User Management',
|
|
563
|
+
description: 'Abilities for managing users',
|
|
564
|
+
},
|
|
565
|
+
];
|
|
566
|
+
|
|
567
|
+
const mockGetAbilityCategories = jest
|
|
568
|
+
.fn()
|
|
569
|
+
.mockResolvedValue( mockCategories );
|
|
570
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
571
|
+
getAbilityCategories: mockGetAbilityCategories,
|
|
572
|
+
} );
|
|
573
|
+
|
|
574
|
+
const result = await getAbilityCategories();
|
|
575
|
+
|
|
576
|
+
expect( resolveSelect ).toHaveBeenCalledWith( store );
|
|
577
|
+
expect( mockGetAbilityCategories ).toHaveBeenCalled();
|
|
578
|
+
expect( result ).toEqual( mockCategories );
|
|
579
|
+
} );
|
|
580
|
+
|
|
581
|
+
it( 'should return empty array when no categories exist', async () => {
|
|
582
|
+
const mockGetAbilityCategories = jest.fn().mockResolvedValue( [] );
|
|
583
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
584
|
+
getAbilityCategories: mockGetAbilityCategories,
|
|
585
|
+
} );
|
|
586
|
+
|
|
587
|
+
const result = await getAbilityCategories();
|
|
588
|
+
|
|
589
|
+
expect( result ).toEqual( [] );
|
|
590
|
+
} );
|
|
591
|
+
} );
|
|
592
|
+
|
|
593
|
+
describe( 'getAbilityCategory', () => {
|
|
594
|
+
it( 'should return a specific category by slug', async () => {
|
|
595
|
+
const mockCategory: AbilityCategory = {
|
|
596
|
+
slug: 'data-retrieval',
|
|
597
|
+
label: 'Data Retrieval',
|
|
598
|
+
description: 'Abilities that retrieve data',
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const mockGetAbilityCategory = jest
|
|
602
|
+
.fn()
|
|
603
|
+
.mockResolvedValue( mockCategory );
|
|
604
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
605
|
+
getAbilityCategory: mockGetAbilityCategory,
|
|
606
|
+
} );
|
|
607
|
+
|
|
608
|
+
const result = await getAbilityCategory( 'data-retrieval' );
|
|
609
|
+
|
|
610
|
+
expect( resolveSelect ).toHaveBeenCalledWith( store );
|
|
611
|
+
expect( mockGetAbilityCategory ).toHaveBeenCalledWith(
|
|
612
|
+
'data-retrieval'
|
|
613
|
+
);
|
|
614
|
+
expect( result ).toEqual( mockCategory );
|
|
615
|
+
} );
|
|
616
|
+
|
|
617
|
+
it( 'should return null if category not found', async () => {
|
|
618
|
+
const mockGetAbilityCategory = jest.fn().mockResolvedValue( null );
|
|
619
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
620
|
+
getAbilityCategory: mockGetAbilityCategory,
|
|
621
|
+
} );
|
|
622
|
+
|
|
623
|
+
const result = await getAbilityCategory( 'non-existent' );
|
|
624
|
+
|
|
625
|
+
expect( mockGetAbilityCategory ).toHaveBeenCalledWith(
|
|
626
|
+
'non-existent'
|
|
627
|
+
);
|
|
628
|
+
expect( result ).toBeNull();
|
|
629
|
+
} );
|
|
630
|
+
|
|
631
|
+
it( 'should handle categories with meta', async () => {
|
|
632
|
+
const mockCategory: AbilityCategory = {
|
|
633
|
+
slug: 'user-management',
|
|
634
|
+
label: 'User Management',
|
|
635
|
+
description: 'Abilities for managing users',
|
|
636
|
+
meta: {
|
|
637
|
+
priority: 'high',
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const mockGetAbilityCategory = jest
|
|
642
|
+
.fn()
|
|
643
|
+
.mockResolvedValue( mockCategory );
|
|
644
|
+
( resolveSelect as jest.Mock ).mockReturnValue( {
|
|
645
|
+
getAbilityCategory: mockGetAbilityCategory,
|
|
646
|
+
} );
|
|
647
|
+
|
|
648
|
+
const result = await getAbilityCategory( 'user-management' );
|
|
649
|
+
|
|
650
|
+
expect( result ).toEqual( mockCategory );
|
|
651
|
+
expect( result?.meta ).toBeDefined();
|
|
652
|
+
expect( result?.meta?.priority ).toBe( 'high' );
|
|
653
|
+
} );
|
|
654
|
+
} );
|
|
655
|
+
} );
|