@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.
Files changed (104) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE.md +788 -0
  3. package/README.md +302 -0
  4. package/build/api.js +189 -0
  5. package/build/api.js.map +7 -0
  6. package/build/index.js +53 -0
  7. package/build/index.js.map +7 -0
  8. package/build/store/actions.js +183 -0
  9. package/build/store/actions.js.map +7 -0
  10. package/build/store/constants.js +58 -0
  11. package/build/store/constants.js.map +7 -0
  12. package/build/store/index.js +77 -0
  13. package/build/store/index.js.map +7 -0
  14. package/build/store/reducer.js +126 -0
  15. package/build/store/reducer.js.map +7 -0
  16. package/build/store/resolvers.js +107 -0
  17. package/build/store/resolvers.js.map +7 -0
  18. package/build/store/selectors.js +64 -0
  19. package/build/store/selectors.js.map +7 -0
  20. package/build/store/tests/actions.test.js +792 -0
  21. package/build/store/tests/actions.test.js.map +7 -0
  22. package/build/store/tests/reducer.test.js +743 -0
  23. package/build/store/tests/reducer.test.js.map +7 -0
  24. package/build/store/tests/resolvers.test.js +520 -0
  25. package/build/store/tests/resolvers.test.js.map +7 -0
  26. package/build/store/tests/selectors.test.js +349 -0
  27. package/build/store/tests/selectors.test.js.map +7 -0
  28. package/build/tests/api.test.js +546 -0
  29. package/build/tests/api.test.js.map +7 -0
  30. package/build/tests/validation.test.js +453 -0
  31. package/build/tests/validation.test.js.map +7 -0
  32. package/build/types.js +19 -0
  33. package/build/types.js.map +7 -0
  34. package/build/validation.js +144 -0
  35. package/build/validation.js.map +7 -0
  36. package/build-module/api.js +146 -0
  37. package/build-module/api.js.map +7 -0
  38. package/build-module/index.js +28 -0
  39. package/build-module/index.js.map +7 -0
  40. package/build-module/store/actions.js +161 -0
  41. package/build-module/store/actions.js.map +7 -0
  42. package/build-module/store/constants.js +24 -0
  43. package/build-module/store/constants.js.map +7 -0
  44. package/build-module/store/index.js +47 -0
  45. package/build-module/store/index.js.map +7 -0
  46. package/build-module/store/reducer.js +112 -0
  47. package/build-module/store/reducer.js.map +7 -0
  48. package/build-module/store/resolvers.js +84 -0
  49. package/build-module/store/resolvers.js.map +7 -0
  50. package/build-module/store/selectors.js +36 -0
  51. package/build-module/store/selectors.js.map +7 -0
  52. package/build-module/store/tests/actions.test.js +804 -0
  53. package/build-module/store/tests/actions.test.js.map +7 -0
  54. package/build-module/store/tests/reducer.test.js +726 -0
  55. package/build-module/store/tests/reducer.test.js.map +7 -0
  56. package/build-module/store/tests/resolvers.test.js +523 -0
  57. package/build-module/store/tests/resolvers.test.js.map +7 -0
  58. package/build-module/store/tests/selectors.test.js +352 -0
  59. package/build-module/store/tests/selectors.test.js.map +7 -0
  60. package/build-module/tests/api.test.js +530 -0
  61. package/build-module/tests/api.test.js.map +7 -0
  62. package/build-module/tests/validation.test.js +451 -0
  63. package/build-module/tests/validation.test.js.map +7 -0
  64. package/build-module/types.js +1 -0
  65. package/build-module/types.js.map +7 -0
  66. package/build-module/validation.js +109 -0
  67. package/build-module/validation.js.map +7 -0
  68. package/build-types/api.d.ts +138 -0
  69. package/build-types/api.d.ts.map +1 -0
  70. package/build-types/index.d.ts +41 -0
  71. package/build-types/index.d.ts.map +1 -0
  72. package/build-types/store/actions.d.ts +76 -0
  73. package/build-types/store/actions.d.ts.map +1 -0
  74. package/build-types/store/constants.d.ts +14 -0
  75. package/build-types/store/constants.d.ts.map +1 -0
  76. package/build-types/store/index.d.ts +7 -0
  77. package/build-types/store/index.d.ts.map +1 -0
  78. package/build-types/store/reducer.d.ts +22 -0
  79. package/build-types/store/reducer.d.ts.map +1 -0
  80. package/build-types/store/resolvers.d.ts +47 -0
  81. package/build-types/store/resolvers.d.ts.map +1 -0
  82. package/build-types/store/selectors.d.ts +37 -0
  83. package/build-types/store/selectors.d.ts.map +1 -0
  84. package/build-types/types.d.ts +163 -0
  85. package/build-types/types.d.ts.map +1 -0
  86. package/build-types/validation.d.ts +23 -0
  87. package/build-types/validation.d.ts.map +1 -0
  88. package/package.json +52 -0
  89. package/src/api.ts +339 -0
  90. package/src/index.ts +65 -0
  91. package/src/store/actions.ts +247 -0
  92. package/src/store/constants.ts +15 -0
  93. package/src/store/index.ts +55 -0
  94. package/src/store/reducer.ts +184 -0
  95. package/src/store/resolvers.ts +151 -0
  96. package/src/store/selectors.ts +82 -0
  97. package/src/store/tests/actions.test.ts +950 -0
  98. package/src/store/tests/reducer.test.ts +861 -0
  99. package/src/store/tests/resolvers.test.ts +642 -0
  100. package/src/store/tests/selectors.test.ts +412 -0
  101. package/src/tests/api.test.ts +655 -0
  102. package/src/tests/validation.test.ts +532 -0
  103. package/src/types.ts +190 -0
  104. package/src/validation.ts +207 -0
@@ -0,0 +1,861 @@
1
+ /**
2
+ * Tests for store reducer.
3
+ */
4
+
5
+ /**
6
+ * Internal dependencies
7
+ */
8
+ import reducer from '../reducer';
9
+ import {
10
+ RECEIVE_ABILITIES,
11
+ REGISTER_ABILITY,
12
+ UNREGISTER_ABILITY,
13
+ RECEIVE_CATEGORIES,
14
+ REGISTER_ABILITY_CATEGORY,
15
+ UNREGISTER_ABILITY_CATEGORY,
16
+ } from '../constants';
17
+
18
+ describe( 'Store Reducer', () => {
19
+ describe( 'abilitiesByName', () => {
20
+ const defaultState = {};
21
+
22
+ describe( 'RECEIVE_ABILITIES', () => {
23
+ it( 'should add abilities to the state', () => {
24
+ const abilities = [
25
+ {
26
+ name: 'test/ability1',
27
+ label: 'Test Ability 1',
28
+ description: 'First test ability',
29
+ input_schema: { type: 'object' },
30
+ },
31
+ {
32
+ name: 'test/ability2',
33
+ label: 'Test Ability 2',
34
+ description: 'Second test ability',
35
+ output_schema: { type: 'object' },
36
+ },
37
+ ];
38
+
39
+ const action = {
40
+ type: RECEIVE_ABILITIES,
41
+ abilities,
42
+ };
43
+
44
+ const state = reducer(
45
+ { abilitiesByName: defaultState },
46
+ action
47
+ );
48
+
49
+ expect( state.abilitiesByName ).toHaveProperty(
50
+ 'test/ability1'
51
+ );
52
+ expect( state.abilitiesByName ).toHaveProperty(
53
+ 'test/ability2'
54
+ );
55
+ expect( state.abilitiesByName[ 'test/ability1' ].label ).toBe(
56
+ 'Test Ability 1'
57
+ );
58
+ expect( state.abilitiesByName[ 'test/ability2' ].label ).toBe(
59
+ 'Test Ability 2'
60
+ );
61
+ } );
62
+
63
+ it( 'should filter out _links from server responses', () => {
64
+ const abilities = [
65
+ {
66
+ name: 'test/ability',
67
+ label: 'Test Ability',
68
+ description: 'Test ability with links',
69
+ _links: {
70
+ self: {
71
+ href: '/wp-abilities/v1/abilities/test/ability',
72
+ },
73
+ collection: { href: '/wp-abilities/v1/abilities' },
74
+ },
75
+ },
76
+ ];
77
+
78
+ const action = {
79
+ type: RECEIVE_ABILITIES,
80
+ abilities,
81
+ };
82
+
83
+ const state = reducer(
84
+ { abilitiesByName: defaultState },
85
+ action
86
+ );
87
+
88
+ expect(
89
+ state.abilitiesByName[ 'test/ability' ]
90
+ ).not.toHaveProperty( '_links' );
91
+ expect( state.abilitiesByName[ 'test/ability' ].name ).toBe(
92
+ 'test/ability'
93
+ );
94
+ expect( state.abilitiesByName[ 'test/ability' ].label ).toBe(
95
+ 'Test Ability'
96
+ );
97
+ } );
98
+
99
+ it( 'should filter out _embedded from server responses', () => {
100
+ const abilities = [
101
+ {
102
+ name: 'test/ability',
103
+ label: 'Test Ability',
104
+ description: 'Test ability with embedded',
105
+ _embedded: {
106
+ author: { id: 1, name: 'Admin' },
107
+ },
108
+ },
109
+ ];
110
+
111
+ const action = {
112
+ type: RECEIVE_ABILITIES,
113
+ abilities,
114
+ };
115
+
116
+ const state = reducer(
117
+ { abilitiesByName: defaultState },
118
+ action
119
+ );
120
+
121
+ expect(
122
+ state.abilitiesByName[ 'test/ability' ]
123
+ ).not.toHaveProperty( '_embedded' );
124
+ } );
125
+
126
+ it( 'should preserve all valid ability properties', () => {
127
+ const abilities = [
128
+ {
129
+ name: 'test/ability',
130
+ label: 'Test Ability',
131
+ description: 'Full test ability.',
132
+ input_schema: { type: 'object' },
133
+ output_schema: { type: 'object' },
134
+ meta: {
135
+ category: 'test',
136
+ },
137
+ callback: () => Promise.resolve( {} ),
138
+ permissionCallback: () => true,
139
+ // Extra properties that should be filtered out
140
+ _links: { self: { href: '/test' } },
141
+ _embedded: { test: 'value' },
142
+ extra_field: 'should be removed',
143
+ },
144
+ ];
145
+
146
+ const action = {
147
+ type: RECEIVE_ABILITIES,
148
+ abilities,
149
+ };
150
+
151
+ const state = reducer(
152
+ { abilitiesByName: defaultState },
153
+ action
154
+ );
155
+ const ability = state.abilitiesByName[ 'test/ability' ];
156
+
157
+ // Should have valid properties
158
+ expect( ability.name ).toBe( 'test/ability' );
159
+ expect( ability.label ).toBe( 'Test Ability' );
160
+ expect( ability.description ).toBe( 'Full test ability.' );
161
+ expect( ability.input_schema ).toEqual( { type: 'object' } );
162
+ expect( ability.output_schema ).toEqual( { type: 'object' } );
163
+ expect( ability.meta ).toEqual( { category: 'test' } );
164
+ expect( ability.callback ).toBeDefined();
165
+ expect( ability.permissionCallback ).toBeDefined();
166
+
167
+ // Should NOT have invalid properties
168
+ expect( ability ).not.toHaveProperty( '_links' );
169
+ expect( ability ).not.toHaveProperty( '_embedded' );
170
+ expect( ability ).not.toHaveProperty( 'extra_field' );
171
+ } );
172
+ } );
173
+
174
+ describe( 'REGISTER_ABILITY', () => {
175
+ it( 'should add ability to the state', () => {
176
+ const ability = {
177
+ name: 'test/ability',
178
+ label: 'Test Ability',
179
+ description: 'Test ability',
180
+ callback: () => Promise.resolve( {} ),
181
+ };
182
+
183
+ const action = {
184
+ type: REGISTER_ABILITY,
185
+ ability,
186
+ };
187
+
188
+ const state = reducer(
189
+ { abilitiesByName: defaultState },
190
+ action
191
+ );
192
+
193
+ expect( state.abilitiesByName ).toHaveProperty(
194
+ 'test/ability'
195
+ );
196
+ expect( state.abilitiesByName[ 'test/ability' ].label ).toBe(
197
+ 'Test Ability'
198
+ );
199
+ } );
200
+
201
+ it( 'should filter out extra properties when registering', () => {
202
+ const ability = {
203
+ name: 'test/ability',
204
+ label: 'Test Ability',
205
+ description: 'Test ability',
206
+ callback: () => Promise.resolve( {} ),
207
+ // Extra properties that should be filtered out
208
+ _links: { self: { href: '/test' } },
209
+ extra_field: 'should be removed',
210
+ };
211
+
212
+ const action = {
213
+ type: REGISTER_ABILITY,
214
+ ability,
215
+ };
216
+
217
+ const state = reducer(
218
+ { abilitiesByName: defaultState },
219
+ action
220
+ );
221
+ const registeredAbility =
222
+ state.abilitiesByName[ 'test/ability' ];
223
+
224
+ // Should have valid properties
225
+ expect( registeredAbility.name ).toBe( 'test/ability' );
226
+ expect( registeredAbility.label ).toBe( 'Test Ability' );
227
+ expect( registeredAbility.description ).toBe( 'Test ability' );
228
+ expect( registeredAbility.callback ).toBeDefined();
229
+
230
+ // Should NOT have invalid properties
231
+ expect( registeredAbility ).not.toHaveProperty( '_links' );
232
+ expect( registeredAbility ).not.toHaveProperty( 'extra_field' );
233
+ } );
234
+
235
+ it( 'should replace existing ability', () => {
236
+ const initialState = {
237
+ 'test/ability': {
238
+ name: 'test/ability',
239
+ label: 'Old Label',
240
+ description: 'Old description',
241
+ },
242
+ };
243
+
244
+ const ability = {
245
+ name: 'test/ability',
246
+ label: 'New Label',
247
+ description: 'New description',
248
+ input_schema: { type: 'string' },
249
+ };
250
+
251
+ const action = {
252
+ type: REGISTER_ABILITY,
253
+ ability,
254
+ };
255
+
256
+ const state = reducer(
257
+ { abilitiesByName: initialState },
258
+ action
259
+ );
260
+
261
+ expect( state.abilitiesByName[ 'test/ability' ].label ).toBe(
262
+ 'New Label'
263
+ );
264
+ expect(
265
+ state.abilitiesByName[ 'test/ability' ].description
266
+ ).toBe( 'New description' );
267
+ expect(
268
+ state.abilitiesByName[ 'test/ability' ].input_schema
269
+ ).toEqual( { type: 'string' } );
270
+ } );
271
+ } );
272
+
273
+ describe( 'UNREGISTER_ABILITY', () => {
274
+ it( 'should remove ability from the state', () => {
275
+ const initialState = {
276
+ 'test/ability1': {
277
+ name: 'test/ability1',
278
+ label: 'Test Ability 1',
279
+ description: 'First test ability',
280
+ },
281
+ 'test/ability2': {
282
+ name: 'test/ability2',
283
+ label: 'Test Ability 2',
284
+ description: 'Second test ability',
285
+ },
286
+ };
287
+
288
+ const action = {
289
+ type: UNREGISTER_ABILITY,
290
+ name: 'test/ability1',
291
+ };
292
+
293
+ const state = reducer(
294
+ { abilitiesByName: initialState },
295
+ action
296
+ );
297
+
298
+ expect( state.abilitiesByName ).not.toHaveProperty(
299
+ 'test/ability1'
300
+ );
301
+ expect( state.abilitiesByName ).toHaveProperty(
302
+ 'test/ability2'
303
+ );
304
+ } );
305
+ } );
306
+
307
+ describe( 'Edge cases', () => {
308
+ it( 'should handle unregistering non-existent ability', () => {
309
+ const initialState = {
310
+ 'test/ability': {
311
+ name: 'test/ability',
312
+ label: 'Test Ability',
313
+ description: 'Test ability',
314
+ },
315
+ };
316
+
317
+ const action = {
318
+ type: UNREGISTER_ABILITY,
319
+ name: 'test/non-existent',
320
+ };
321
+
322
+ const state = reducer(
323
+ { abilitiesByName: initialState },
324
+ action
325
+ );
326
+
327
+ expect( state.abilitiesByName ).toEqual( initialState );
328
+ } );
329
+
330
+ it( 'should handle undefined abilities in RECEIVE_ABILITIES', () => {
331
+ const action = {
332
+ type: RECEIVE_ABILITIES,
333
+ abilities: undefined,
334
+ };
335
+
336
+ const state = reducer(
337
+ { abilitiesByName: defaultState },
338
+ action
339
+ );
340
+
341
+ expect( state.abilitiesByName ).toEqual( defaultState );
342
+ } );
343
+
344
+ it( 'should handle undefined ability in REGISTER_ABILITY', () => {
345
+ const action = {
346
+ type: REGISTER_ABILITY,
347
+ ability: undefined,
348
+ };
349
+
350
+ const state = reducer(
351
+ { abilitiesByName: defaultState },
352
+ action
353
+ );
354
+
355
+ expect( state.abilitiesByName ).toEqual( defaultState );
356
+ } );
357
+
358
+ it( 'should handle undefined properties gracefully', () => {
359
+ const abilities = [
360
+ {
361
+ name: 'test/minimal',
362
+ label: 'Minimal',
363
+ description:
364
+ 'Minimal ability with undefined properties',
365
+ input_schema: undefined,
366
+ output_schema: undefined,
367
+ meta: undefined,
368
+ callback: undefined,
369
+ permissionCallback: undefined,
370
+ },
371
+ ];
372
+
373
+ const action = {
374
+ type: RECEIVE_ABILITIES,
375
+ abilities,
376
+ };
377
+
378
+ const state = reducer(
379
+ { abilitiesByName: defaultState },
380
+ action
381
+ );
382
+ const ability = state.abilitiesByName[ 'test/minimal' ];
383
+
384
+ expect( ability.name ).toBe( 'test/minimal' );
385
+ expect( ability.label ).toBe( 'Minimal' );
386
+ expect( ability.description ).toBe(
387
+ 'Minimal ability with undefined properties'
388
+ );
389
+ // Undefined properties should not be present
390
+ expect( ability ).not.toHaveProperty( 'input_schema' );
391
+ expect( ability ).not.toHaveProperty( 'output_schema' );
392
+ expect( ability ).not.toHaveProperty( 'meta' );
393
+ expect( ability ).not.toHaveProperty( 'callback' );
394
+ expect( ability ).not.toHaveProperty( 'permissionCallback' );
395
+ } );
396
+ } );
397
+ } );
398
+
399
+ describe( 'categoriesBySlug', () => {
400
+ const defaultState = {};
401
+
402
+ describe( 'RECEIVE_CATEGORIES', () => {
403
+ it( 'should add categories to the state', () => {
404
+ const categories = [
405
+ {
406
+ slug: 'data-retrieval',
407
+ label: 'Data Retrieval',
408
+ description: 'Abilities that retrieve data',
409
+ },
410
+ {
411
+ slug: 'user-management',
412
+ label: 'User Management',
413
+ description: 'Abilities for managing users',
414
+ },
415
+ ];
416
+
417
+ const action = {
418
+ type: RECEIVE_CATEGORIES,
419
+ categories,
420
+ };
421
+
422
+ const state = reducer(
423
+ { categoriesBySlug: defaultState, abilitiesByName: {} },
424
+ action
425
+ );
426
+
427
+ expect( state.categoriesBySlug ).toHaveProperty(
428
+ 'data-retrieval'
429
+ );
430
+ expect( state.categoriesBySlug ).toHaveProperty(
431
+ 'user-management'
432
+ );
433
+ expect( state.categoriesBySlug[ 'data-retrieval' ].label ).toBe(
434
+ 'Data Retrieval'
435
+ );
436
+ expect(
437
+ state.categoriesBySlug[ 'user-management' ].label
438
+ ).toBe( 'User Management' );
439
+ } );
440
+
441
+ it( 'should filter out _links from server responses', () => {
442
+ const categories = [
443
+ {
444
+ slug: 'data-retrieval',
445
+ label: 'Data Retrieval',
446
+ description: 'Test category with links',
447
+ _links: {
448
+ self: {
449
+ href: '/wp-abilities/v1/categories/data-retrieval',
450
+ },
451
+ collection: {
452
+ href: '/wp-abilities/v1/categories',
453
+ },
454
+ },
455
+ },
456
+ ];
457
+
458
+ const action = {
459
+ type: RECEIVE_CATEGORIES,
460
+ categories,
461
+ };
462
+
463
+ const state = reducer(
464
+ { categoriesBySlug: defaultState, abilitiesByName: {} },
465
+ action
466
+ );
467
+
468
+ expect(
469
+ state.categoriesBySlug[ 'data-retrieval' ]
470
+ ).not.toHaveProperty( '_links' );
471
+ expect( state.categoriesBySlug[ 'data-retrieval' ].slug ).toBe(
472
+ 'data-retrieval'
473
+ );
474
+ expect( state.categoriesBySlug[ 'data-retrieval' ].label ).toBe(
475
+ 'Data Retrieval'
476
+ );
477
+ } );
478
+
479
+ it( 'should filter out _embedded from server responses', () => {
480
+ const categories = [
481
+ {
482
+ slug: 'data-retrieval',
483
+ label: 'Data Retrieval',
484
+ description: 'Test category with embedded',
485
+ _embedded: {
486
+ author: { id: 1, name: 'Admin' },
487
+ },
488
+ },
489
+ ];
490
+
491
+ const action = {
492
+ type: RECEIVE_CATEGORIES,
493
+ categories,
494
+ };
495
+
496
+ const state = reducer(
497
+ { categoriesBySlug: defaultState, abilitiesByName: {} },
498
+ action
499
+ );
500
+
501
+ expect(
502
+ state.categoriesBySlug[ 'data-retrieval' ]
503
+ ).not.toHaveProperty( '_embedded' );
504
+ } );
505
+
506
+ it( 'should preserve all valid category properties', () => {
507
+ const categories = [
508
+ {
509
+ slug: 'data-retrieval',
510
+ label: 'Data Retrieval',
511
+ description: 'Full test category.',
512
+ meta: {
513
+ priority: 'high',
514
+ color: 'blue',
515
+ },
516
+ // Extra properties that should be filtered out
517
+ _links: { self: { href: '/test' } },
518
+ _embedded: { test: 'value' },
519
+ extra_field: 'should be removed',
520
+ },
521
+ ];
522
+
523
+ const action = {
524
+ type: RECEIVE_CATEGORIES,
525
+ categories,
526
+ };
527
+
528
+ const state = reducer(
529
+ { categoriesBySlug: defaultState, abilitiesByName: {} },
530
+ action
531
+ );
532
+ const category = state.categoriesBySlug[ 'data-retrieval' ];
533
+
534
+ // Should have valid properties
535
+ expect( category.slug ).toBe( 'data-retrieval' );
536
+ expect( category.label ).toBe( 'Data Retrieval' );
537
+ expect( category.description ).toBe( 'Full test category.' );
538
+ expect( category.meta ).toEqual( {
539
+ priority: 'high',
540
+ color: 'blue',
541
+ } );
542
+
543
+ // Should NOT have invalid properties
544
+ expect( category ).not.toHaveProperty( '_links' );
545
+ expect( category ).not.toHaveProperty( '_embedded' );
546
+ expect( category ).not.toHaveProperty( 'extra_field' );
547
+ } );
548
+
549
+ it( 'should overwrite existing categories', () => {
550
+ const initialState = {
551
+ 'existing-category': {
552
+ slug: 'existing-category',
553
+ label: 'Existing Category',
554
+ description: 'Already in store',
555
+ },
556
+ };
557
+
558
+ const categories = [
559
+ {
560
+ slug: 'data-retrieval',
561
+ label: 'Data Retrieval',
562
+ description: 'New category',
563
+ },
564
+ ];
565
+
566
+ const action = {
567
+ type: RECEIVE_CATEGORIES,
568
+ categories,
569
+ };
570
+
571
+ const state = reducer(
572
+ { categoriesBySlug: initialState, abilitiesByName: {} },
573
+ action
574
+ );
575
+
576
+ // Should only have new categories, old ones are replaced
577
+ expect( state.categoriesBySlug ).not.toHaveProperty(
578
+ 'existing-category'
579
+ );
580
+ expect( state.categoriesBySlug ).toHaveProperty(
581
+ 'data-retrieval'
582
+ );
583
+ } );
584
+ } );
585
+
586
+ describe( 'Edge cases', () => {
587
+ it( 'should handle undefined categories in RECEIVE_CATEGORIES', () => {
588
+ const action = {
589
+ type: RECEIVE_CATEGORIES,
590
+ categories: undefined,
591
+ };
592
+
593
+ const state = reducer(
594
+ { categoriesBySlug: defaultState, abilitiesByName: {} },
595
+ action
596
+ );
597
+
598
+ expect( state.categoriesBySlug ).toEqual( defaultState );
599
+ } );
600
+
601
+ it( 'should handle empty categories array', () => {
602
+ const action = {
603
+ type: RECEIVE_CATEGORIES,
604
+ categories: [],
605
+ };
606
+
607
+ const state = reducer(
608
+ { categoriesBySlug: defaultState, abilitiesByName: {} },
609
+ action
610
+ );
611
+
612
+ expect( state.categoriesBySlug ).toEqual( defaultState );
613
+ } );
614
+
615
+ it( 'should handle undefined properties gracefully', () => {
616
+ const categories = [
617
+ {
618
+ slug: 'minimal',
619
+ label: 'Minimal',
620
+ description: 'Minimal category with undefined meta',
621
+ meta: undefined,
622
+ },
623
+ ];
624
+
625
+ const action = {
626
+ type: RECEIVE_CATEGORIES,
627
+ categories,
628
+ };
629
+
630
+ const state = reducer(
631
+ { categoriesBySlug: defaultState, abilitiesByName: {} },
632
+ action
633
+ );
634
+ const category = state.categoriesBySlug.minimal;
635
+
636
+ expect( category.slug ).toBe( 'minimal' );
637
+ expect( category.label ).toBe( 'Minimal' );
638
+ expect( category.description ).toBe(
639
+ 'Minimal category with undefined meta'
640
+ );
641
+ // Undefined properties should not be present
642
+ expect( category ).not.toHaveProperty( 'meta' );
643
+ } );
644
+ } );
645
+
646
+ describe( 'REGISTER_ABILITY_CATEGORY', () => {
647
+ it( 'should add category to the state', () => {
648
+ const category = {
649
+ slug: 'test-category',
650
+ label: 'Test Category',
651
+ description: 'A test category',
652
+ };
653
+
654
+ const action = {
655
+ type: REGISTER_ABILITY_CATEGORY,
656
+ category,
657
+ };
658
+
659
+ const state = reducer(
660
+ { categoriesBySlug: defaultState, abilitiesByName: {} },
661
+ action
662
+ );
663
+
664
+ expect( state.categoriesBySlug ).toHaveProperty(
665
+ 'test-category'
666
+ );
667
+ expect( state.categoriesBySlug[ 'test-category' ].label ).toBe(
668
+ 'Test Category'
669
+ );
670
+ } );
671
+
672
+ it( 'should add category with meta to the state', () => {
673
+ const category = {
674
+ slug: 'test-category',
675
+ label: 'Test Category',
676
+ description: 'A test category',
677
+ meta: { color: 'blue', priority: 'high' },
678
+ };
679
+
680
+ const action = {
681
+ type: REGISTER_ABILITY_CATEGORY,
682
+ category,
683
+ };
684
+
685
+ const state = reducer(
686
+ { categoriesBySlug: defaultState, abilitiesByName: {} },
687
+ action
688
+ );
689
+
690
+ expect(
691
+ state.categoriesBySlug[ 'test-category' ].meta
692
+ ).toEqual( { color: 'blue', priority: 'high' } );
693
+ } );
694
+
695
+ it( 'should filter out extra properties when registering', () => {
696
+ const category = {
697
+ slug: 'test-category',
698
+ label: 'Test Category',
699
+ description: 'A test category',
700
+ // Extra properties that should be filtered out
701
+ _links: { self: { href: '/test' } },
702
+ _embedded: { author: { id: 1 } },
703
+ extra_field: 'should be removed',
704
+ };
705
+
706
+ const action = {
707
+ type: REGISTER_ABILITY_CATEGORY,
708
+ category,
709
+ };
710
+
711
+ const state = reducer(
712
+ { categoriesBySlug: defaultState, abilitiesByName: {} },
713
+ action
714
+ );
715
+ const registeredCategory =
716
+ state.categoriesBySlug[ 'test-category' ];
717
+
718
+ // Should have valid properties
719
+ expect( registeredCategory.slug ).toBe( 'test-category' );
720
+ expect( registeredCategory.label ).toBe( 'Test Category' );
721
+ expect( registeredCategory.description ).toBe(
722
+ 'A test category'
723
+ );
724
+
725
+ // Should NOT have invalid properties
726
+ expect( registeredCategory ).not.toHaveProperty( '_links' );
727
+ expect( registeredCategory ).not.toHaveProperty( '_embedded' );
728
+ expect( registeredCategory ).not.toHaveProperty(
729
+ 'extra_field'
730
+ );
731
+ } );
732
+
733
+ it( 'should replace existing category', () => {
734
+ const initialState = {
735
+ 'test-category': {
736
+ slug: 'test-category',
737
+ label: 'Old Label',
738
+ description: 'Old description',
739
+ },
740
+ };
741
+
742
+ const category = {
743
+ slug: 'test-category',
744
+ label: 'New Label',
745
+ description: 'New description',
746
+ meta: { color: 'red' },
747
+ };
748
+
749
+ const action = {
750
+ type: REGISTER_ABILITY_CATEGORY,
751
+ category,
752
+ };
753
+
754
+ const state = reducer(
755
+ { categoriesBySlug: initialState, abilitiesByName: {} },
756
+ action
757
+ );
758
+
759
+ expect( state.categoriesBySlug[ 'test-category' ].label ).toBe(
760
+ 'New Label'
761
+ );
762
+ expect(
763
+ state.categoriesBySlug[ 'test-category' ].description
764
+ ).toBe( 'New description' );
765
+ expect(
766
+ state.categoriesBySlug[ 'test-category' ].meta
767
+ ).toEqual( { color: 'red' } );
768
+ } );
769
+
770
+ it( 'should handle undefined category', () => {
771
+ const action = {
772
+ type: REGISTER_ABILITY_CATEGORY,
773
+ category: undefined,
774
+ };
775
+
776
+ const state = reducer(
777
+ { categoriesBySlug: defaultState, abilitiesByName: {} },
778
+ action
779
+ );
780
+
781
+ expect( state.categoriesBySlug ).toEqual( defaultState );
782
+ } );
783
+ } );
784
+
785
+ describe( 'UNREGISTER_ABILITY_CATEGORY', () => {
786
+ it( 'should remove category from the state', () => {
787
+ const initialState = {
788
+ category1: {
789
+ slug: 'category1',
790
+ label: 'Category 1',
791
+ description: 'First category',
792
+ },
793
+ category2: {
794
+ slug: 'category2',
795
+ label: 'Category 2',
796
+ description: 'Second category',
797
+ },
798
+ };
799
+
800
+ const action = {
801
+ type: UNREGISTER_ABILITY_CATEGORY,
802
+ slug: 'category1',
803
+ };
804
+
805
+ const state = reducer(
806
+ { categoriesBySlug: initialState, abilitiesByName: {} },
807
+ action
808
+ );
809
+
810
+ expect( state.categoriesBySlug ).not.toHaveProperty(
811
+ 'category1'
812
+ );
813
+ expect( state.categoriesBySlug ).toHaveProperty( 'category2' );
814
+ } );
815
+
816
+ it( 'should handle unregistering non-existent category', () => {
817
+ const initialState = {
818
+ 'test-category': {
819
+ slug: 'test-category',
820
+ label: 'Test Category',
821
+ description: 'A test category',
822
+ },
823
+ };
824
+
825
+ const action = {
826
+ type: UNREGISTER_ABILITY_CATEGORY,
827
+ slug: 'non-existent',
828
+ };
829
+
830
+ const state = reducer(
831
+ { categoriesBySlug: initialState, abilitiesByName: {} },
832
+ action
833
+ );
834
+
835
+ expect( state.categoriesBySlug ).toEqual( initialState );
836
+ } );
837
+
838
+ it( 'should handle undefined slug', () => {
839
+ const initialState = {
840
+ 'test-category': {
841
+ slug: 'test-category',
842
+ label: 'Test Category',
843
+ description: 'A test category',
844
+ },
845
+ };
846
+
847
+ const action = {
848
+ type: UNREGISTER_ABILITY_CATEGORY,
849
+ slug: undefined,
850
+ };
851
+
852
+ const state = reducer(
853
+ { categoriesBySlug: initialState, abilitiesByName: {} },
854
+ action
855
+ );
856
+
857
+ expect( state.categoriesBySlug ).toEqual( initialState );
858
+ } );
859
+ } );
860
+ } );
861
+ } );