@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,532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for schema validation utilities.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal dependencies
|
|
7
|
+
*/
|
|
8
|
+
import { validateValueFromSchema } from '../validation';
|
|
9
|
+
|
|
10
|
+
describe( 'validateValueFromSchema', () => {
|
|
11
|
+
describe( 'type validation', () => {
|
|
12
|
+
it( 'should validate string type', () => {
|
|
13
|
+
const schema = { type: 'string' };
|
|
14
|
+
expect( validateValueFromSchema( 'hello', schema ) ).toBe( true );
|
|
15
|
+
expect( validateValueFromSchema( 123, schema ) ).toBe(
|
|
16
|
+
' is not of type string.'
|
|
17
|
+
);
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
it( 'should validate number type', () => {
|
|
21
|
+
const schema = { type: 'number' };
|
|
22
|
+
expect( validateValueFromSchema( 123, schema ) ).toBe( true );
|
|
23
|
+
expect( validateValueFromSchema( 45.67, schema ) ).toBe( true );
|
|
24
|
+
expect( validateValueFromSchema( 'hello', schema ) ).toBe(
|
|
25
|
+
' is not of type number.'
|
|
26
|
+
);
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
it( 'should validate boolean type', () => {
|
|
30
|
+
const schema = { type: 'boolean' };
|
|
31
|
+
expect( validateValueFromSchema( true, schema ) ).toBe( true );
|
|
32
|
+
expect( validateValueFromSchema( false, schema ) ).toBe( true );
|
|
33
|
+
expect( validateValueFromSchema( 'true', schema ) ).toBe(
|
|
34
|
+
' is not of type boolean.'
|
|
35
|
+
);
|
|
36
|
+
} );
|
|
37
|
+
|
|
38
|
+
it( 'should validate array type', () => {
|
|
39
|
+
const schema = { type: 'array' };
|
|
40
|
+
expect( validateValueFromSchema( [ 1, 2, 3 ], schema ) ).toBe(
|
|
41
|
+
true
|
|
42
|
+
);
|
|
43
|
+
expect( validateValueFromSchema( {}, schema ) ).toBe(
|
|
44
|
+
' is not of type array.'
|
|
45
|
+
);
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
it( 'should validate object type', () => {
|
|
49
|
+
const schema = { type: 'object' };
|
|
50
|
+
expect( validateValueFromSchema( { a: 1 }, schema ) ).toBe( true );
|
|
51
|
+
expect( validateValueFromSchema( [], schema ) ).toBe(
|
|
52
|
+
' is not of type object.'
|
|
53
|
+
);
|
|
54
|
+
} );
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
describe( 'object validation', () => {
|
|
58
|
+
it( 'should validate required properties', () => {
|
|
59
|
+
const schema = {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
name: { type: 'string' },
|
|
63
|
+
age: { type: 'number' },
|
|
64
|
+
},
|
|
65
|
+
required: [ 'name' ],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
expect(
|
|
69
|
+
validateValueFromSchema( { name: 'John', age: 30 }, schema )
|
|
70
|
+
).toBe( true );
|
|
71
|
+
expect( validateValueFromSchema( { name: 'John' }, schema ) ).toBe(
|
|
72
|
+
true
|
|
73
|
+
);
|
|
74
|
+
expect( validateValueFromSchema( { age: 30 }, schema ) ).toBe(
|
|
75
|
+
'name is a required property of .'
|
|
76
|
+
);
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
it( 'should validate nested objects', () => {
|
|
80
|
+
const schema = {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
user: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
name: { type: 'string' },
|
|
87
|
+
},
|
|
88
|
+
required: [ 'name' ],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
expect(
|
|
94
|
+
validateValueFromSchema( { user: { name: 'John' } }, schema )
|
|
95
|
+
).toBe( true );
|
|
96
|
+
expect( validateValueFromSchema( { user: {} }, schema ) ).toBe(
|
|
97
|
+
'name is a required property of [user].'
|
|
98
|
+
);
|
|
99
|
+
} );
|
|
100
|
+
|
|
101
|
+
it( 'should validate additionalProperties: false', () => {
|
|
102
|
+
const schema = {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
name: { type: 'string' },
|
|
106
|
+
},
|
|
107
|
+
additionalProperties: false,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
expect( validateValueFromSchema( { name: 'John' }, schema ) ).toBe(
|
|
111
|
+
true
|
|
112
|
+
);
|
|
113
|
+
expect(
|
|
114
|
+
validateValueFromSchema(
|
|
115
|
+
{ name: 'John', extra: 'value' },
|
|
116
|
+
schema
|
|
117
|
+
)
|
|
118
|
+
).toBe( 'extra is not a valid property of Object.' );
|
|
119
|
+
} );
|
|
120
|
+
} );
|
|
121
|
+
|
|
122
|
+
describe( 'array validation', () => {
|
|
123
|
+
it( 'should validate array items', () => {
|
|
124
|
+
const schema = {
|
|
125
|
+
type: 'array',
|
|
126
|
+
items: { type: 'number' },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
expect( validateValueFromSchema( [ 1, 2, 3 ], schema ) ).toBe(
|
|
130
|
+
true
|
|
131
|
+
);
|
|
132
|
+
expect( validateValueFromSchema( [ 1, 'two', 3 ], schema ) ).toBe(
|
|
133
|
+
'[1] is not of type number.'
|
|
134
|
+
);
|
|
135
|
+
} );
|
|
136
|
+
} );
|
|
137
|
+
|
|
138
|
+
describe( 'edge cases', () => {
|
|
139
|
+
it( 'should pass validation when empty schema provided', () => {
|
|
140
|
+
const consoleSpy = jest
|
|
141
|
+
.spyOn( console, 'warn' )
|
|
142
|
+
.mockImplementation();
|
|
143
|
+
|
|
144
|
+
expect( validateValueFromSchema( 'anything', {} ) ).toBe( true );
|
|
145
|
+
expect( consoleSpy ).toHaveBeenCalledWith(
|
|
146
|
+
'The "type" schema keyword for value is required.'
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
consoleSpy.mockRestore();
|
|
150
|
+
} );
|
|
151
|
+
|
|
152
|
+
it( 'should warn when type is missing but still pass validation', () => {
|
|
153
|
+
const consoleSpy = jest
|
|
154
|
+
.spyOn( console, 'warn' )
|
|
155
|
+
.mockImplementation();
|
|
156
|
+
const schema = { properties: { name: { type: 'string' } } };
|
|
157
|
+
const result = validateValueFromSchema( { name: 'test' }, schema );
|
|
158
|
+
|
|
159
|
+
expect( result ).toBe( true );
|
|
160
|
+
expect( consoleSpy ).toHaveBeenCalledWith(
|
|
161
|
+
'The "type" schema keyword for value is required.'
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
consoleSpy.mockRestore();
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
it( 'should include param name in warning when provided', () => {
|
|
168
|
+
const consoleSpy = jest
|
|
169
|
+
.spyOn( console, 'warn' )
|
|
170
|
+
.mockImplementation();
|
|
171
|
+
const schema = { format: 'email' }; // Schema without type
|
|
172
|
+
const result = validateValueFromSchema(
|
|
173
|
+
'test@example.com',
|
|
174
|
+
schema,
|
|
175
|
+
'email_field'
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect( result ).toBe( true );
|
|
179
|
+
expect( consoleSpy ).toHaveBeenCalledWith(
|
|
180
|
+
'The "type" schema keyword for email_field is required.'
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
consoleSpy.mockRestore();
|
|
184
|
+
} );
|
|
185
|
+
|
|
186
|
+
it( 'should handle null values correctly', () => {
|
|
187
|
+
const schema = { type: [ 'string', 'null' ] };
|
|
188
|
+
expect( validateValueFromSchema( null, schema ) ).toBe( true );
|
|
189
|
+
expect( validateValueFromSchema( 'hello', schema ) ).toBe( true );
|
|
190
|
+
} );
|
|
191
|
+
} );
|
|
192
|
+
|
|
193
|
+
describe( 'AI-relevant format validation', () => {
|
|
194
|
+
it( 'should validate email format', () => {
|
|
195
|
+
const schema = { type: 'string', format: 'email' };
|
|
196
|
+
expect(
|
|
197
|
+
validateValueFromSchema( 'user@example.com', schema )
|
|
198
|
+
).toBe( true );
|
|
199
|
+
expect( validateValueFromSchema( 'invalid-email', schema ) ).toBe(
|
|
200
|
+
'Invalid email address.'
|
|
201
|
+
);
|
|
202
|
+
} );
|
|
203
|
+
|
|
204
|
+
it( 'should validate date-time format', () => {
|
|
205
|
+
const schema = { type: 'string', format: 'date-time' };
|
|
206
|
+
expect(
|
|
207
|
+
validateValueFromSchema( '2024-01-15T10:30:00Z', schema )
|
|
208
|
+
).toBe( true );
|
|
209
|
+
expect(
|
|
210
|
+
validateValueFromSchema( '2024-01-15T10:30:00.123Z', schema )
|
|
211
|
+
).toBe( true );
|
|
212
|
+
expect( validateValueFromSchema( 'not-a-date', schema ) ).toBe(
|
|
213
|
+
'Invalid date.'
|
|
214
|
+
);
|
|
215
|
+
} );
|
|
216
|
+
|
|
217
|
+
it( 'should validate UUID format', () => {
|
|
218
|
+
const schema = { type: 'string', format: 'uuid' };
|
|
219
|
+
expect(
|
|
220
|
+
validateValueFromSchema(
|
|
221
|
+
'123e4567-e89b-12d3-a456-426614174000',
|
|
222
|
+
schema
|
|
223
|
+
)
|
|
224
|
+
).toBe( true );
|
|
225
|
+
expect( validateValueFromSchema( 'not-a-uuid', schema ) ).toBe(
|
|
226
|
+
' is not a valid UUID.'
|
|
227
|
+
);
|
|
228
|
+
} );
|
|
229
|
+
|
|
230
|
+
it( 'should validate IPv4 format', () => {
|
|
231
|
+
const schema = { type: 'string', format: 'ipv4' };
|
|
232
|
+
expect( validateValueFromSchema( '192.168.1.1', schema ) ).toBe(
|
|
233
|
+
true
|
|
234
|
+
);
|
|
235
|
+
expect( validateValueFromSchema( '256.256.256.256', schema ) ).toBe(
|
|
236
|
+
' is not a valid IP address.'
|
|
237
|
+
);
|
|
238
|
+
} );
|
|
239
|
+
|
|
240
|
+
it( 'should validate hostname format', () => {
|
|
241
|
+
const schema = { type: 'string', format: 'hostname' };
|
|
242
|
+
expect( validateValueFromSchema( 'example.com', schema ) ).toBe(
|
|
243
|
+
true
|
|
244
|
+
);
|
|
245
|
+
expect( validateValueFromSchema( 'sub.example.com', schema ) ).toBe(
|
|
246
|
+
true
|
|
247
|
+
);
|
|
248
|
+
expect( validateValueFromSchema( 'not a hostname!', schema ) ).toBe(
|
|
249
|
+
' is not a valid hostname.'
|
|
250
|
+
);
|
|
251
|
+
} );
|
|
252
|
+
} );
|
|
253
|
+
|
|
254
|
+
describe( 'pattern validation', () => {
|
|
255
|
+
it( 'should validate string patterns', () => {
|
|
256
|
+
const schema = { type: 'string', pattern: '^[A-Z][a-z]+$' };
|
|
257
|
+
expect( validateValueFromSchema( 'Hello', schema ) ).toBe( true );
|
|
258
|
+
expect( validateValueFromSchema( 'hello', schema ) ).toBe(
|
|
259
|
+
' does not match pattern ^[A-Z][a-z]+$.'
|
|
260
|
+
);
|
|
261
|
+
} );
|
|
262
|
+
} );
|
|
263
|
+
|
|
264
|
+
describe( 'number constraints', () => {
|
|
265
|
+
it( 'should validate minimum and maximum', () => {
|
|
266
|
+
const schema = { type: 'number', minimum: 0, maximum: 100 };
|
|
267
|
+
expect( validateValueFromSchema( 50, schema ) ).toBe( true );
|
|
268
|
+
expect( validateValueFromSchema( 0, schema ) ).toBe( true );
|
|
269
|
+
expect( validateValueFromSchema( 100, schema ) ).toBe( true );
|
|
270
|
+
expect( validateValueFromSchema( -1, schema ) ).toBe(
|
|
271
|
+
' must be greater than or equal to 0'
|
|
272
|
+
);
|
|
273
|
+
expect( validateValueFromSchema( 101, schema ) ).toBe(
|
|
274
|
+
' must be less than or equal to 100'
|
|
275
|
+
);
|
|
276
|
+
} );
|
|
277
|
+
|
|
278
|
+
it( 'should validate multipleOf', () => {
|
|
279
|
+
const schema = { type: 'number', multipleOf: 5 };
|
|
280
|
+
expect( validateValueFromSchema( 10, schema ) ).toBe( true );
|
|
281
|
+
expect( validateValueFromSchema( 15, schema ) ).toBe( true );
|
|
282
|
+
expect( validateValueFromSchema( 7, schema ) ).toBe(
|
|
283
|
+
' must be a multiple of 5.'
|
|
284
|
+
);
|
|
285
|
+
} );
|
|
286
|
+
} );
|
|
287
|
+
|
|
288
|
+
describe( 'enum validation', () => {
|
|
289
|
+
it( 'should validate enum values', () => {
|
|
290
|
+
const schema = {
|
|
291
|
+
type: 'string',
|
|
292
|
+
enum: [ 'read', 'write', 'execute' ],
|
|
293
|
+
};
|
|
294
|
+
expect( validateValueFromSchema( 'read', schema ) ).toBe( true );
|
|
295
|
+
expect( validateValueFromSchema( 'delete', schema ) ).toBe(
|
|
296
|
+
' is not one of read, write, execute.'
|
|
297
|
+
);
|
|
298
|
+
} );
|
|
299
|
+
} );
|
|
300
|
+
|
|
301
|
+
describe( 'anyOf validation', () => {
|
|
302
|
+
it( 'should validate anyOf schemas', () => {
|
|
303
|
+
const schema = {
|
|
304
|
+
anyOf: [ { type: 'string' }, { type: 'number' } ],
|
|
305
|
+
};
|
|
306
|
+
expect( validateValueFromSchema( 'hello', schema ) ).toBe( true );
|
|
307
|
+
expect( validateValueFromSchema( 42, schema ) ).toBe( true );
|
|
308
|
+
expect( validateValueFromSchema( true, schema ) ).toBe(
|
|
309
|
+
' is invalid (failed anyOf validation).'
|
|
310
|
+
);
|
|
311
|
+
} );
|
|
312
|
+
} );
|
|
313
|
+
|
|
314
|
+
describe( 'strict JSON validation (no type coercion)', () => {
|
|
315
|
+
it( 'should NOT coerce string "true" to boolean', () => {
|
|
316
|
+
const schema = { type: 'boolean' };
|
|
317
|
+
expect( validateValueFromSchema( true, schema ) ).toBe( true );
|
|
318
|
+
expect( validateValueFromSchema( false, schema ) ).toBe( true );
|
|
319
|
+
expect( validateValueFromSchema( 'true', schema ) ).toBe(
|
|
320
|
+
' is not of type boolean.'
|
|
321
|
+
);
|
|
322
|
+
expect( validateValueFromSchema( '1', schema ) ).toBe(
|
|
323
|
+
' is not of type boolean.'
|
|
324
|
+
);
|
|
325
|
+
} );
|
|
326
|
+
|
|
327
|
+
it( 'should NOT accept empty string as object', () => {
|
|
328
|
+
const schema = { type: 'object' };
|
|
329
|
+
expect( validateValueFromSchema( {}, schema ) ).toBe( true );
|
|
330
|
+
expect( validateValueFromSchema( '', schema ) ).toBe(
|
|
331
|
+
' is not of type object.'
|
|
332
|
+
);
|
|
333
|
+
} );
|
|
334
|
+
} );
|
|
335
|
+
|
|
336
|
+
describe( 'string length constraints', () => {
|
|
337
|
+
it( 'should validate minLength constraint', () => {
|
|
338
|
+
const schema = { type: 'string', minLength: 3 };
|
|
339
|
+
expect( validateValueFromSchema( 'hello', schema ) ).toBe( true );
|
|
340
|
+
expect( validateValueFromSchema( 'hi', schema ) ).toBe(
|
|
341
|
+
' must be at least 3 characters long.'
|
|
342
|
+
);
|
|
343
|
+
} );
|
|
344
|
+
|
|
345
|
+
it( 'should validate maxLength constraint', () => {
|
|
346
|
+
const schema = { type: 'string', maxLength: 5 };
|
|
347
|
+
expect( validateValueFromSchema( 'hello', schema ) ).toBe( true );
|
|
348
|
+
expect( validateValueFromSchema( 'hello world', schema ) ).toBe(
|
|
349
|
+
' must be at most 5 characters long.'
|
|
350
|
+
);
|
|
351
|
+
} );
|
|
352
|
+
|
|
353
|
+
it( 'should handle singular character in length messages', () => {
|
|
354
|
+
const minSchema = { type: 'string', minLength: 1 };
|
|
355
|
+
expect( validateValueFromSchema( '', minSchema ) ).toBe(
|
|
356
|
+
' must be at least 1 character long.'
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const maxSchema = { type: 'string', maxLength: 1 };
|
|
360
|
+
expect( validateValueFromSchema( 'ab', maxSchema ) ).toBe(
|
|
361
|
+
' must be at most 1 character long.'
|
|
362
|
+
);
|
|
363
|
+
} );
|
|
364
|
+
} );
|
|
365
|
+
|
|
366
|
+
describe( 'array item constraints', () => {
|
|
367
|
+
it( 'should validate minItems constraint', () => {
|
|
368
|
+
const schema = { type: 'array', minItems: 2 };
|
|
369
|
+
expect( validateValueFromSchema( [ 1, 2, 3 ], schema ) ).toBe(
|
|
370
|
+
true
|
|
371
|
+
);
|
|
372
|
+
expect( validateValueFromSchema( [ 1 ], schema ) ).toBe(
|
|
373
|
+
' must contain at least 2 items.'
|
|
374
|
+
);
|
|
375
|
+
} );
|
|
376
|
+
|
|
377
|
+
it( 'should validate maxItems constraint', () => {
|
|
378
|
+
const schema = { type: 'array', maxItems: 3 };
|
|
379
|
+
expect( validateValueFromSchema( [ 1, 2, 3 ], schema ) ).toBe(
|
|
380
|
+
true
|
|
381
|
+
);
|
|
382
|
+
expect( validateValueFromSchema( [ 1, 2, 3, 4 ], schema ) ).toBe(
|
|
383
|
+
' must contain at most 3 items.'
|
|
384
|
+
);
|
|
385
|
+
} );
|
|
386
|
+
|
|
387
|
+
it( 'should validate uniqueItems constraint', () => {
|
|
388
|
+
const schema = { type: 'array', uniqueItems: true };
|
|
389
|
+
expect( validateValueFromSchema( [ 1, 2, 3 ], schema ) ).toBe(
|
|
390
|
+
true
|
|
391
|
+
);
|
|
392
|
+
expect( validateValueFromSchema( [ 1, 2, 2, 3 ], schema ) ).toBe(
|
|
393
|
+
' has duplicate items.'
|
|
394
|
+
);
|
|
395
|
+
} );
|
|
396
|
+
|
|
397
|
+
it( 'should handle singular item in item messages', () => {
|
|
398
|
+
const minSchema = { type: 'array', minItems: 1 };
|
|
399
|
+
expect( validateValueFromSchema( [], minSchema ) ).toBe(
|
|
400
|
+
' must contain at least 1 item.'
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const maxSchema = { type: 'array', maxItems: 1 };
|
|
404
|
+
expect( validateValueFromSchema( [ 1, 2 ], maxSchema ) ).toBe(
|
|
405
|
+
' must contain at most 1 item.'
|
|
406
|
+
);
|
|
407
|
+
} );
|
|
408
|
+
} );
|
|
409
|
+
|
|
410
|
+
describe( 'object property constraints', () => {
|
|
411
|
+
it( 'should validate minProperties constraint', () => {
|
|
412
|
+
const schema = { type: 'object', minProperties: 2 };
|
|
413
|
+
expect( validateValueFromSchema( { a: 1, b: 2 }, schema ) ).toBe(
|
|
414
|
+
true
|
|
415
|
+
);
|
|
416
|
+
expect( validateValueFromSchema( { a: 1 }, schema ) ).toBe(
|
|
417
|
+
' must contain at least 2 properties.'
|
|
418
|
+
);
|
|
419
|
+
} );
|
|
420
|
+
|
|
421
|
+
it( 'should validate maxProperties constraint', () => {
|
|
422
|
+
const schema = { type: 'object', maxProperties: 2 };
|
|
423
|
+
expect( validateValueFromSchema( { a: 1, b: 2 }, schema ) ).toBe(
|
|
424
|
+
true
|
|
425
|
+
);
|
|
426
|
+
expect(
|
|
427
|
+
validateValueFromSchema( { a: 1, b: 2, c: 3 }, schema )
|
|
428
|
+
).toBe( ' must contain at most 2 properties.' );
|
|
429
|
+
} );
|
|
430
|
+
|
|
431
|
+
it( 'should handle singular property in property messages', () => {
|
|
432
|
+
const minSchema = { type: 'object', minProperties: 1 };
|
|
433
|
+
expect( validateValueFromSchema( {}, minSchema ) ).toBe(
|
|
434
|
+
' must contain at least 1 property.'
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const maxSchema = { type: 'object', maxProperties: 1 };
|
|
438
|
+
expect( validateValueFromSchema( { a: 1, b: 2 }, maxSchema ) ).toBe(
|
|
439
|
+
' must contain at most 1 property.'
|
|
440
|
+
);
|
|
441
|
+
} );
|
|
442
|
+
} );
|
|
443
|
+
|
|
444
|
+
describe( 'schema edge cases and errors', () => {
|
|
445
|
+
it( 'should handle empty schema object as valid but warn about missing type', () => {
|
|
446
|
+
const consoleSpy = jest
|
|
447
|
+
.spyOn( console, 'warn' )
|
|
448
|
+
.mockImplementation();
|
|
449
|
+
|
|
450
|
+
// Empty object schema triggers warning about missing type
|
|
451
|
+
expect( validateValueFromSchema( 'anything', {} ) ).toBe( true );
|
|
452
|
+
expect( consoleSpy ).toHaveBeenCalledWith(
|
|
453
|
+
'The "type" schema keyword for value is required.'
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
consoleSpy.mockRestore();
|
|
457
|
+
} );
|
|
458
|
+
|
|
459
|
+
it( 'should warn for invalid schema types but still pass validation', () => {
|
|
460
|
+
const consoleSpy = jest
|
|
461
|
+
.spyOn( console, 'warn' )
|
|
462
|
+
.mockImplementation();
|
|
463
|
+
|
|
464
|
+
// Testing edge cases where schema is not a valid object
|
|
465
|
+
expect(
|
|
466
|
+
validateValueFromSchema(
|
|
467
|
+
'anything',
|
|
468
|
+
undefined as unknown as Record< string, any >
|
|
469
|
+
)
|
|
470
|
+
).toBe( true );
|
|
471
|
+
expect( consoleSpy ).toHaveBeenCalledWith(
|
|
472
|
+
'Schema must be an object. Received undefined.'
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
consoleSpy.mockClear();
|
|
476
|
+
expect(
|
|
477
|
+
validateValueFromSchema(
|
|
478
|
+
123,
|
|
479
|
+
null as unknown as Record< string, any >
|
|
480
|
+
)
|
|
481
|
+
).toBe( true );
|
|
482
|
+
expect( consoleSpy ).toHaveBeenCalledWith(
|
|
483
|
+
'Schema must be an object. Received object.' // typeof null === 'object'
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
consoleSpy.mockClear();
|
|
487
|
+
expect(
|
|
488
|
+
validateValueFromSchema(
|
|
489
|
+
true,
|
|
490
|
+
false as unknown as Record< string, any >
|
|
491
|
+
)
|
|
492
|
+
).toBe( true );
|
|
493
|
+
expect( consoleSpy ).toHaveBeenCalledWith(
|
|
494
|
+
'Schema must be an object. Received boolean.'
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
consoleSpy.mockRestore();
|
|
498
|
+
} );
|
|
499
|
+
|
|
500
|
+
it( 'should handle schema compilation errors', () => {
|
|
501
|
+
// Pass an invalid schema that will cause compilation error
|
|
502
|
+
const invalidSchema = { type: 'invalid-type' };
|
|
503
|
+
const consoleErrorSpy = jest
|
|
504
|
+
.spyOn( console, 'error' )
|
|
505
|
+
.mockImplementation();
|
|
506
|
+
|
|
507
|
+
const result = validateValueFromSchema( 'test', invalidSchema );
|
|
508
|
+
|
|
509
|
+
expect( result ).toBe( 'Invalid schema provided for validation.' );
|
|
510
|
+
expect( consoleErrorSpy ).toHaveBeenCalledWith(
|
|
511
|
+
'Schema compilation error:',
|
|
512
|
+
expect.any( Error )
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
consoleErrorSpy.mockRestore();
|
|
516
|
+
} );
|
|
517
|
+
|
|
518
|
+
it( 'should handle const validation keyword', () => {
|
|
519
|
+
const schema = {
|
|
520
|
+
type: 'string',
|
|
521
|
+
const: 'exact-value',
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
expect( validateValueFromSchema( 'exact-value', schema ) ).toBe(
|
|
525
|
+
true
|
|
526
|
+
);
|
|
527
|
+
expect( validateValueFromSchema( 'different-value', schema ) ).toBe(
|
|
528
|
+
'must be equal to constant'
|
|
529
|
+
);
|
|
530
|
+
} );
|
|
531
|
+
} );
|
|
532
|
+
} );
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress Abilities API Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Callback function for client-side abilities.
|
|
7
|
+
*/
|
|
8
|
+
export type AbilityCallback = (
|
|
9
|
+
input: AbilityInput
|
|
10
|
+
) => AbilityOutput | Promise< AbilityOutput >;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Permission callback function for client-side abilities.
|
|
14
|
+
* Returns true if the ability can be executed, false otherwise.
|
|
15
|
+
*/
|
|
16
|
+
export type PermissionCallback = (
|
|
17
|
+
input?: AbilityInput
|
|
18
|
+
) => boolean | Promise< boolean >;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Represents an ability in the WordPress Abilities API.
|
|
22
|
+
*
|
|
23
|
+
* @see WP_Ability
|
|
24
|
+
*/
|
|
25
|
+
export interface Ability {
|
|
26
|
+
/**
|
|
27
|
+
* The unique name/identifier of the ability, with its namespace.
|
|
28
|
+
* Example: 'my-plugin/my-ability'
|
|
29
|
+
* @see WP_Ability::get_name()
|
|
30
|
+
*/
|
|
31
|
+
name: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The human-readable label for the ability.
|
|
35
|
+
* @see WP_Ability::get_label()
|
|
36
|
+
*/
|
|
37
|
+
label: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The detailed description of the ability.
|
|
41
|
+
* @see WP_Ability::get_description()
|
|
42
|
+
*/
|
|
43
|
+
description: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The category this ability belongs to.
|
|
47
|
+
* Must be a valid category slug (lowercase alphanumeric with dashes).
|
|
48
|
+
* Example: 'data-retrieval', 'user-management'
|
|
49
|
+
* @see WP_Ability::get_category()
|
|
50
|
+
*/
|
|
51
|
+
category: string;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* JSON Schema for the ability's input parameters.
|
|
55
|
+
* @see WP_Ability::get_input_schema()
|
|
56
|
+
*/
|
|
57
|
+
input_schema?: Record< string, any >;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* JSON Schema for the ability's output format.
|
|
61
|
+
* @see WP_Ability::get_output_schema()
|
|
62
|
+
*/
|
|
63
|
+
output_schema?: Record< string, any >;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Callback function for client-side abilities.
|
|
67
|
+
* If present, the ability will be executed locally in the browser.
|
|
68
|
+
* If not present, the ability will be executed via REST API on the server.
|
|
69
|
+
*/
|
|
70
|
+
callback?: AbilityCallback;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Client Permission callback for abilities.
|
|
74
|
+
* Called before executing the ability to check if it's allowed.
|
|
75
|
+
* If it returns false, the ability execution will be denied.
|
|
76
|
+
*/
|
|
77
|
+
permissionCallback?: PermissionCallback;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Metadata about the ability.
|
|
81
|
+
* @see WP_Ability::get_meta()
|
|
82
|
+
*/
|
|
83
|
+
meta?: {
|
|
84
|
+
annotations?: {
|
|
85
|
+
readonly?: boolean | null;
|
|
86
|
+
destructive?: boolean | null;
|
|
87
|
+
idempotent?: boolean | null;
|
|
88
|
+
};
|
|
89
|
+
[ key: string ]: any;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The shape of the arguments for querying abilities.
|
|
95
|
+
*/
|
|
96
|
+
export interface AbilitiesQueryArgs {
|
|
97
|
+
/**
|
|
98
|
+
* Optional category slug to filter abilities.
|
|
99
|
+
*/
|
|
100
|
+
category?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Represents an ability category in the WordPress Abilities API.
|
|
105
|
+
*
|
|
106
|
+
* @see WP_Ability_Category
|
|
107
|
+
*/
|
|
108
|
+
export interface AbilityCategory {
|
|
109
|
+
/**
|
|
110
|
+
* The unique slug identifier for the category.
|
|
111
|
+
* Must be lowercase alphanumeric with dashes only.
|
|
112
|
+
* Example: 'data-retrieval', 'user-management'
|
|
113
|
+
* @see WP_Ability_Category::get_slug()
|
|
114
|
+
*/
|
|
115
|
+
slug: string;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The human-readable label for the category.
|
|
119
|
+
* @see WP_Ability_Category::get_label()
|
|
120
|
+
*/
|
|
121
|
+
label: string;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The detailed description of the category.
|
|
125
|
+
* @see WP_Ability_Category::get_description()
|
|
126
|
+
*/
|
|
127
|
+
description: string;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Metadata about the category.
|
|
131
|
+
* @see WP_Ability_Category::get_meta()
|
|
132
|
+
*/
|
|
133
|
+
meta?: Record< string, any >;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Arguments for registering an ability category.
|
|
138
|
+
* Matches the server-side wp_register_ability_category() $args parameter.
|
|
139
|
+
*
|
|
140
|
+
* @see wp_register_ability_category()
|
|
141
|
+
*/
|
|
142
|
+
export interface AbilityCategoryArgs {
|
|
143
|
+
/**
|
|
144
|
+
* The human-readable label for the category.
|
|
145
|
+
*/
|
|
146
|
+
label: string;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The detailed description of the category.
|
|
150
|
+
*/
|
|
151
|
+
description: string;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Optional metadata about the category.
|
|
155
|
+
*/
|
|
156
|
+
meta?: Record< string, any >;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The state shape for the abilities store.
|
|
161
|
+
*/
|
|
162
|
+
export interface AbilitiesState {
|
|
163
|
+
/**
|
|
164
|
+
* Map of ability names to ability objects.
|
|
165
|
+
*/
|
|
166
|
+
abilitiesByName: Record< string, Ability >;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Map of category slugs to category objects.
|
|
170
|
+
*/
|
|
171
|
+
categoriesBySlug: Record< string, AbilityCategory >;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Input parameters for ability execution.
|
|
176
|
+
* Can be any JSON-serializable value: primitive, array, object, or null.
|
|
177
|
+
*/
|
|
178
|
+
export type AbilityInput = any;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Result from ability execution.
|
|
182
|
+
* The actual shape depends on the ability's output schema.
|
|
183
|
+
*/
|
|
184
|
+
export type AbilityOutput = any;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Validation error - just a message string.
|
|
188
|
+
* The Abilities API wraps this with the appropriate error code.
|
|
189
|
+
*/
|
|
190
|
+
export type ValidationError = string;
|