@object-ui/core 3.1.5 → 3.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.
Files changed (110) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +20 -1
  3. package/dist/actions/ActionRunner.d.ts +9 -0
  4. package/dist/actions/ActionRunner.js +41 -4
  5. package/dist/adapters/ValueDataSource.d.ts +5 -1
  6. package/dist/adapters/ValueDataSource.js +30 -1
  7. package/dist/errors/index.js +2 -3
  8. package/dist/evaluator/ExpressionCache.d.ts +9 -10
  9. package/dist/evaluator/ExpressionCache.js +29 -8
  10. package/dist/evaluator/SafeExpressionParser.d.ts +131 -0
  11. package/dist/evaluator/SafeExpressionParser.js +851 -0
  12. package/dist/evaluator/index.d.ts +1 -0
  13. package/dist/evaluator/index.js +1 -0
  14. package/dist/protocols/DndProtocol.js +2 -14
  15. package/dist/protocols/KeyboardProtocol.js +1 -4
  16. package/dist/protocols/NotificationProtocol.js +3 -13
  17. package/dist/utils/debug.js +2 -1
  18. package/dist/utils/filter-converter.js +25 -5
  19. package/package.json +33 -9
  20. package/.turbo/turbo-build.log +0 -4
  21. package/src/__benchmarks__/core.bench.ts +0 -64
  22. package/src/__tests__/protocols/DndProtocol.test.ts +0 -186
  23. package/src/__tests__/protocols/KeyboardProtocol.test.ts +0 -177
  24. package/src/__tests__/protocols/NotificationProtocol.test.ts +0 -142
  25. package/src/__tests__/protocols/ResponsiveProtocol.test.ts +0 -176
  26. package/src/__tests__/protocols/SharingProtocol.test.ts +0 -188
  27. package/src/actions/ActionEngine.ts +0 -268
  28. package/src/actions/ActionRunner.ts +0 -717
  29. package/src/actions/TransactionManager.ts +0 -521
  30. package/src/actions/UndoManager.ts +0 -215
  31. package/src/actions/__tests__/ActionEngine.test.ts +0 -206
  32. package/src/actions/__tests__/ActionRunner.params.test.ts +0 -134
  33. package/src/actions/__tests__/ActionRunner.test.ts +0 -711
  34. package/src/actions/__tests__/TransactionManager.test.ts +0 -447
  35. package/src/actions/__tests__/UndoManager.test.ts +0 -320
  36. package/src/actions/index.ts +0 -12
  37. package/src/adapters/ApiDataSource.ts +0 -376
  38. package/src/adapters/README.md +0 -180
  39. package/src/adapters/ValueDataSource.ts +0 -438
  40. package/src/adapters/__tests__/ApiDataSource.test.ts +0 -418
  41. package/src/adapters/__tests__/ValueDataSource.test.ts +0 -472
  42. package/src/adapters/__tests__/resolveDataSource.test.ts +0 -144
  43. package/src/adapters/index.ts +0 -15
  44. package/src/adapters/resolveDataSource.ts +0 -79
  45. package/src/builder/__tests__/schema-builder.test.ts +0 -235
  46. package/src/builder/schema-builder.ts +0 -584
  47. package/src/data-scope/DataScopeManager.ts +0 -269
  48. package/src/data-scope/ViewDataProvider.ts +0 -282
  49. package/src/data-scope/__tests__/DataScopeManager.test.ts +0 -211
  50. package/src/data-scope/__tests__/ViewDataProvider.test.ts +0 -270
  51. package/src/data-scope/index.ts +0 -24
  52. package/src/errors/__tests__/errors.test.ts +0 -292
  53. package/src/errors/index.ts +0 -270
  54. package/src/evaluator/ExpressionCache.ts +0 -192
  55. package/src/evaluator/ExpressionContext.ts +0 -118
  56. package/src/evaluator/ExpressionEvaluator.ts +0 -315
  57. package/src/evaluator/FormulaFunctions.ts +0 -398
  58. package/src/evaluator/__tests__/ExpressionCache.test.ts +0 -135
  59. package/src/evaluator/__tests__/ExpressionContext.test.ts +0 -110
  60. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +0 -131
  61. package/src/evaluator/__tests__/FormulaFunctions.test.ts +0 -447
  62. package/src/evaluator/index.ts +0 -12
  63. package/src/index.ts +0 -38
  64. package/src/protocols/DndProtocol.ts +0 -184
  65. package/src/protocols/KeyboardProtocol.ts +0 -185
  66. package/src/protocols/NotificationProtocol.ts +0 -159
  67. package/src/protocols/ResponsiveProtocol.ts +0 -210
  68. package/src/protocols/SharingProtocol.ts +0 -185
  69. package/src/protocols/index.ts +0 -13
  70. package/src/query/__tests__/query-ast.test.ts +0 -211
  71. package/src/query/__tests__/window-functions.test.ts +0 -275
  72. package/src/query/index.ts +0 -7
  73. package/src/query/query-ast.ts +0 -341
  74. package/src/registry/PluginScopeImpl.ts +0 -259
  75. package/src/registry/PluginSystem.ts +0 -206
  76. package/src/registry/Registry.ts +0 -219
  77. package/src/registry/WidgetRegistry.ts +0 -316
  78. package/src/registry/__tests__/PluginSystem.test.ts +0 -309
  79. package/src/registry/__tests__/Registry.test.ts +0 -293
  80. package/src/registry/__tests__/WidgetRegistry.test.ts +0 -321
  81. package/src/registry/__tests__/plugin-scope-integration.test.ts +0 -283
  82. package/src/theme/ThemeEngine.ts +0 -530
  83. package/src/theme/__tests__/ThemeEngine.test.ts +0 -668
  84. package/src/theme/index.ts +0 -24
  85. package/src/types/index.ts +0 -21
  86. package/src/utils/__tests__/debug-collector.test.ts +0 -102
  87. package/src/utils/__tests__/debug.test.ts +0 -134
  88. package/src/utils/__tests__/expand-fields.test.ts +0 -120
  89. package/src/utils/__tests__/extract-records.test.ts +0 -50
  90. package/src/utils/__tests__/filter-converter.test.ts +0 -118
  91. package/src/utils/__tests__/merge-views-into-objects.test.ts +0 -110
  92. package/src/utils/__tests__/normalize-quick-filter.test.ts +0 -123
  93. package/src/utils/debug-collector.ts +0 -100
  94. package/src/utils/debug.ts +0 -147
  95. package/src/utils/expand-fields.ts +0 -76
  96. package/src/utils/extract-records.ts +0 -33
  97. package/src/utils/filter-converter.ts +0 -133
  98. package/src/utils/merge-views-into-objects.ts +0 -36
  99. package/src/utils/normalize-quick-filter.ts +0 -78
  100. package/src/validation/__tests__/object-validation-engine.test.ts +0 -567
  101. package/src/validation/__tests__/schema-validator.test.ts +0 -118
  102. package/src/validation/__tests__/validation-engine.test.ts +0 -102
  103. package/src/validation/index.ts +0 -10
  104. package/src/validation/schema-validator.ts +0 -344
  105. package/src/validation/validation-engine.ts +0 -528
  106. package/src/validation/validators/index.ts +0 -25
  107. package/src/validation/validators/object-validation-engine.ts +0 -722
  108. package/tsconfig.json +0 -15
  109. package/tsconfig.tsbuildinfo +0 -1
  110. package/vitest.config.ts +0 -2
@@ -1,711 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect, vi, beforeEach } from 'vitest';
10
- import {
11
- ActionRunner,
12
- executeAction,
13
- type ActionDef,
14
- type ActionContext,
15
- type ActionResult,
16
- } from '../ActionRunner';
17
-
18
- describe('ActionRunner', () => {
19
- let runner: ActionRunner;
20
- let context: ActionContext;
21
-
22
- beforeEach(() => {
23
- context = {
24
- data: { id: 1, name: 'Test' },
25
- record: { id: 1, status: 'active' },
26
- user: { id: 'u1', role: 'admin' },
27
- };
28
- runner = new ActionRunner(context);
29
- });
30
-
31
- // ==========================================================================
32
- // Basic execution
33
- // ==========================================================================
34
-
35
- describe('basic execution', () => {
36
- it('should execute an onClick callback', async () => {
37
- const onClick = vi.fn();
38
- const result = await runner.execute({ onClick });
39
- expect(result.success).toBe(true);
40
- expect(onClick).toHaveBeenCalledOnce();
41
- });
42
-
43
- it('should handle async onClick', async () => {
44
- const onClick = vi.fn().mockResolvedValue(undefined);
45
- const result = await runner.execute({ onClick });
46
- expect(result.success).toBe(true);
47
- expect(onClick).toHaveBeenCalledOnce();
48
- });
49
-
50
- it('should catch errors and return failure', async () => {
51
- const onClick = vi.fn().mockRejectedValue(new Error('boom'));
52
- const result = await runner.execute({ onClick });
53
- expect(result.success).toBe(false);
54
- expect(result.error).toBe('boom');
55
- });
56
- });
57
-
58
- // ==========================================================================
59
- // Conditions and disabled
60
- // ==========================================================================
61
-
62
- describe('conditions', () => {
63
- it('should skip action when condition evaluates to false', async () => {
64
- const onClick = vi.fn();
65
- const result = await runner.execute({
66
- condition: '${record.status === "inactive"}',
67
- onClick,
68
- });
69
- expect(result.success).toBe(false);
70
- expect(result.error).toBe('Action condition not met');
71
- expect(onClick).not.toHaveBeenCalled();
72
- });
73
-
74
- it('should execute action when condition evaluates to true', async () => {
75
- const onClick = vi.fn();
76
- const result = await runner.execute({
77
- condition: '${record.status === "active"}',
78
- onClick,
79
- });
80
- expect(result.success).toBe(true);
81
- expect(onClick).toHaveBeenCalledOnce();
82
- });
83
-
84
- it('should skip action when disabled is true (boolean)', async () => {
85
- const onClick = vi.fn();
86
- const result = await runner.execute({ disabled: true, onClick });
87
- expect(result.success).toBe(false);
88
- expect(result.error).toBe('Action is disabled');
89
- expect(onClick).not.toHaveBeenCalled();
90
- });
91
-
92
- it('should skip action when disabled expression evaluates to true', async () => {
93
- const onClick = vi.fn();
94
- const result = await runner.execute({
95
- disabled: '${user.role === "admin"}',
96
- onClick,
97
- });
98
- expect(result.success).toBe(false);
99
- expect(onClick).not.toHaveBeenCalled();
100
- });
101
- });
102
-
103
- // ==========================================================================
104
- // Confirmation
105
- // ==========================================================================
106
-
107
- describe('confirmation', () => {
108
- it('should show confirmation and proceed when accepted', async () => {
109
- const confirmHandler = vi.fn().mockResolvedValue(true);
110
- runner.setConfirmHandler(confirmHandler);
111
-
112
- const onClick = vi.fn();
113
- const result = await runner.execute({
114
- confirmText: 'Are you sure?',
115
- onClick,
116
- });
117
-
118
- expect(confirmHandler).toHaveBeenCalledWith('Are you sure?', undefined);
119
- expect(result.success).toBe(true);
120
- expect(onClick).toHaveBeenCalledOnce();
121
- });
122
-
123
- it('should cancel when confirmation is rejected', async () => {
124
- const confirmHandler = vi.fn().mockResolvedValue(false);
125
- runner.setConfirmHandler(confirmHandler);
126
-
127
- const onClick = vi.fn();
128
- const result = await runner.execute({
129
- confirmText: 'Are you sure?',
130
- onClick,
131
- });
132
-
133
- expect(result.success).toBe(false);
134
- expect(result.error).toBe('Action cancelled by user');
135
- expect(onClick).not.toHaveBeenCalled();
136
- });
137
-
138
- it('should support structured confirmation', async () => {
139
- const confirmHandler = vi.fn().mockResolvedValue(true);
140
- runner.setConfirmHandler(confirmHandler);
141
-
142
- await runner.execute({
143
- confirm: {
144
- title: 'Delete',
145
- message: 'Delete this item?',
146
- confirmText: 'Yes, delete',
147
- cancelText: 'Cancel',
148
- },
149
- onClick: vi.fn(),
150
- });
151
-
152
- expect(confirmHandler).toHaveBeenCalledWith('Delete this item?', {
153
- title: 'Delete',
154
- confirmText: 'Yes, delete',
155
- cancelText: 'Cancel',
156
- });
157
- });
158
- });
159
-
160
- // ==========================================================================
161
- // Custom handlers
162
- // ==========================================================================
163
-
164
- describe('custom handlers', () => {
165
- it('should dispatch to registered custom handler', async () => {
166
- const handler = vi.fn().mockResolvedValue({ success: true, data: 42 });
167
- runner.registerHandler('my-action', handler);
168
-
169
- const action: ActionDef = { type: 'my-action', params: { foo: 'bar' } };
170
- const result = await runner.execute(action);
171
-
172
- expect(handler).toHaveBeenCalledWith(action, context);
173
- expect(result.success).toBe(true);
174
- expect(result.data).toBe(42);
175
- });
176
-
177
- it('should allow unregistering a handler', async () => {
178
- const handler = vi.fn().mockResolvedValue({ success: true });
179
- runner.registerHandler('temp', handler);
180
- runner.unregisterHandler('temp');
181
-
182
- const result = await runner.execute({ type: 'temp', onClick: vi.fn() });
183
- expect(handler).not.toHaveBeenCalled();
184
- });
185
- });
186
-
187
- // ==========================================================================
188
- // Script action type
189
- // ==========================================================================
190
-
191
- describe('script action type', () => {
192
- it('should evaluate script expression', async () => {
193
- const result = await runner.execute({
194
- type: 'script',
195
- execute: 'record.id + 100',
196
- });
197
- expect(result.success).toBe(true);
198
- expect(result.data).toBe(101);
199
- });
200
-
201
- it('should evaluate script with string target fallback', async () => {
202
- const result = await runner.execute({
203
- type: 'script',
204
- target: 'data.name',
205
- });
206
- expect(result.success).toBe(true);
207
- expect(result.data).toBe('Test');
208
- });
209
-
210
- it('should fail when no script provided', async () => {
211
- const result = await runner.execute({ type: 'script' });
212
- expect(result.success).toBe(false);
213
- expect(result.error).toContain('No script provided');
214
- });
215
-
216
- it('should return data as undefined for expressions referencing missing vars', async () => {
217
- const result = await runner.execute({
218
- type: 'script',
219
- execute: 'data.nonExistent',
220
- });
221
- // ExpressionEvaluator returns undefined for missing properties (doesn't throw)
222
- expect(result.success).toBe(true);
223
- expect(result.data).toBeUndefined();
224
- });
225
- });
226
-
227
- // ==========================================================================
228
- // URL action type
229
- // ==========================================================================
230
-
231
- describe('url action type', () => {
232
- it('should return redirect for relative URL', async () => {
233
- const result = await runner.execute({
234
- type: 'url',
235
- target: '/dashboard',
236
- });
237
- expect(result.success).toBe(true);
238
- expect(result.redirect).toBe('/dashboard');
239
- });
240
-
241
- it('should use navigation handler when provided', async () => {
242
- const navHandler = vi.fn();
243
- runner.setNavigationHandler(navHandler);
244
-
245
- const result = await runner.execute({
246
- type: 'url',
247
- target: '/dashboard',
248
- });
249
-
250
- expect(result.success).toBe(true);
251
- expect(navHandler).toHaveBeenCalledWith('/dashboard', {
252
- external: false,
253
- newTab: false,
254
- });
255
- });
256
-
257
- it('should detect external URLs', async () => {
258
- const navHandler = vi.fn();
259
- runner.setNavigationHandler(navHandler);
260
-
261
- await runner.execute({
262
- type: 'url',
263
- target: 'https://example.com',
264
- });
265
-
266
- expect(navHandler).toHaveBeenCalledWith('https://example.com', {
267
- external: true,
268
- newTab: true,
269
- });
270
- });
271
-
272
- it('should reject javascript: URLs', async () => {
273
- const result = await runner.execute({
274
- type: 'url',
275
- target: 'javascript:alert(1)',
276
- });
277
- expect(result.success).toBe(false);
278
- expect(result.error).toContain('Invalid URL');
279
- });
280
-
281
- it('should fail when no URL provided', async () => {
282
- const result = await runner.execute({ type: 'url' });
283
- expect(result.success).toBe(false);
284
- expect(result.error).toContain('No URL provided');
285
- });
286
- });
287
-
288
- // ==========================================================================
289
- // Modal action type
290
- // ==========================================================================
291
-
292
- describe('modal action type', () => {
293
- it('should return modal schema when no handler registered', async () => {
294
- const modalSchema = { type: 'dialog', title: 'Edit' };
295
- const result = await runner.execute({
296
- type: 'modal',
297
- modal: modalSchema,
298
- });
299
- expect(result.success).toBe(true);
300
- expect(result.modal).toEqual(modalSchema);
301
- });
302
-
303
- it('should delegate to modal handler when provided', async () => {
304
- const modalHandler = vi.fn().mockResolvedValue({ success: true, data: { saved: true } });
305
- runner.setModalHandler(modalHandler);
306
-
307
- const modalSchema = { type: 'form', fields: [] };
308
- const result = await runner.execute({
309
- type: 'modal',
310
- modal: modalSchema,
311
- });
312
-
313
- expect(modalHandler).toHaveBeenCalledWith(modalSchema, context);
314
- expect(result.success).toBe(true);
315
- expect(result.data).toEqual({ saved: true });
316
- });
317
-
318
- it('should use target as modal schema fallback', async () => {
319
- const result = await runner.execute({
320
- type: 'modal',
321
- target: 'edit_form',
322
- });
323
- expect(result.success).toBe(true);
324
- expect(result.modal).toBe('edit_form');
325
- });
326
-
327
- it('should fail when no modal schema/target provided', async () => {
328
- const result = await runner.execute({ type: 'modal' });
329
- expect(result.success).toBe(false);
330
- expect(result.error).toContain('No modal schema');
331
- });
332
- });
333
-
334
- // ==========================================================================
335
- // Flow action type
336
- // ==========================================================================
337
-
338
- describe('flow action type', () => {
339
- it('should delegate to registered flow handler', async () => {
340
- const flowHandler = vi.fn().mockResolvedValue({ success: true, data: 'flow_started' });
341
- runner.registerHandler('flow', flowHandler);
342
-
343
- const action: ActionDef = { type: 'flow', target: 'approval_flow' };
344
- const result = await runner.execute(action);
345
-
346
- expect(flowHandler).toHaveBeenCalledWith(action, context);
347
- expect(result.success).toBe(true);
348
- });
349
-
350
- it('should fail when no flow handler registered', async () => {
351
- const result = await runner.execute({ type: 'flow', target: 'my_flow' });
352
- expect(result.success).toBe(false);
353
- expect(result.error).toContain('Flow handler not registered');
354
- });
355
-
356
- it('should fail when no flow target provided', async () => {
357
- const result = await runner.execute({ type: 'flow' });
358
- expect(result.success).toBe(false);
359
- expect(result.error).toContain('No flow target');
360
- });
361
- });
362
-
363
- // ==========================================================================
364
- // API action type
365
- // ==========================================================================
366
-
367
- describe('api action type', () => {
368
- it('should call fetch with simple string endpoint', async () => {
369
- const mockResponse = { ok: true, json: vi.fn().mockResolvedValue({ id: 1 }) };
370
- global.fetch = vi.fn().mockResolvedValue(mockResponse);
371
-
372
- const result = await runner.execute({
373
- type: 'api',
374
- api: '/api/records',
375
- method: 'GET',
376
- });
377
-
378
- expect(global.fetch).toHaveBeenCalledWith('/api/records', expect.objectContaining({
379
- method: 'GET',
380
- }));
381
- expect(result.success).toBe(true);
382
- expect(result.data).toEqual({ id: 1 });
383
- });
384
-
385
- it('should use endpoint field as alias', async () => {
386
- const mockResponse = { ok: true, json: vi.fn().mockResolvedValue({ ok: true }) };
387
- global.fetch = vi.fn().mockResolvedValue(mockResponse);
388
-
389
- const result = await runner.execute({
390
- type: 'api',
391
- endpoint: '/api/v2/records',
392
- });
393
-
394
- expect(global.fetch).toHaveBeenCalledWith('/api/v2/records', expect.any(Object));
395
- expect(result.success).toBe(true);
396
- });
397
-
398
- it('should support complex API config', async () => {
399
- const mockResponse = { ok: true, json: vi.fn().mockResolvedValue({ done: true }) };
400
- global.fetch = vi.fn().mockResolvedValue(mockResponse);
401
-
402
- const result = await runner.execute({
403
- type: 'api',
404
- api: {
405
- url: '/api/records',
406
- method: 'PUT',
407
- headers: { Authorization: 'Bearer xyz' },
408
- body: { name: 'Updated' },
409
- queryParams: { include: 'details' },
410
- },
411
- });
412
-
413
- expect(global.fetch).toHaveBeenCalledWith(
414
- '/api/records?include=details',
415
- expect.objectContaining({
416
- method: 'PUT',
417
- headers: expect.objectContaining({ Authorization: 'Bearer xyz' }),
418
- body: JSON.stringify({ name: 'Updated' }),
419
- }),
420
- );
421
- expect(result.success).toBe(true);
422
- });
423
-
424
- it('should handle HTTP errors', async () => {
425
- global.fetch = vi.fn().mockResolvedValue({
426
- ok: false,
427
- status: 404,
428
- statusText: 'Not Found',
429
- });
430
-
431
- const result = await runner.execute({
432
- type: 'api',
433
- api: '/api/missing',
434
- });
435
-
436
- expect(result.success).toBe(false);
437
- expect(result.error).toContain('404');
438
- });
439
-
440
- it('should handle network errors', async () => {
441
- global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
442
-
443
- const result = await runner.execute({
444
- type: 'api',
445
- api: '/api/records',
446
- });
447
-
448
- expect(result.success).toBe(false);
449
- expect(result.error).toBe('Network error');
450
- });
451
-
452
- it('should fail when no endpoint provided', async () => {
453
- const result = await runner.execute({ type: 'api' });
454
- expect(result.success).toBe(false);
455
- expect(result.error).toContain('No API endpoint');
456
- });
457
- });
458
-
459
- // ==========================================================================
460
- // Navigation action type
461
- // ==========================================================================
462
-
463
- describe('navigation action type', () => {
464
- it('should return redirect for internal navigation', async () => {
465
- const result = await runner.execute({
466
- type: 'navigation',
467
- navigate: { to: '/records/1' },
468
- });
469
- expect(result.success).toBe(true);
470
- expect(result.redirect).toBe('/records/1');
471
- });
472
-
473
- it('should use navigation handler when provided', async () => {
474
- const navHandler = vi.fn();
475
- runner.setNavigationHandler(navHandler);
476
-
477
- await runner.execute({
478
- type: 'navigation',
479
- navigate: { to: '/records/1', replace: true },
480
- });
481
-
482
- expect(navHandler).toHaveBeenCalledWith('/records/1', expect.objectContaining({
483
- replace: true,
484
- }));
485
- });
486
-
487
- it('should reject invalid URLs', async () => {
488
- const result = await runner.execute({
489
- type: 'navigation',
490
- navigate: { to: 'data:text/html,...' },
491
- });
492
- expect(result.success).toBe(false);
493
- expect(result.error).toContain('Invalid URL');
494
- });
495
- });
496
-
497
- // ==========================================================================
498
- // Post-execution: toast, refreshAfter
499
- // ==========================================================================
500
-
501
- describe('post-execution', () => {
502
- it('should emit success toast', async () => {
503
- const toastHandler = vi.fn();
504
- runner.setToastHandler(toastHandler);
505
-
506
- await runner.execute({
507
- onClick: vi.fn(),
508
- successMessage: 'Saved!',
509
- });
510
-
511
- expect(toastHandler).toHaveBeenCalledWith('Saved!', { type: 'success', duration: undefined });
512
- });
513
-
514
- it('should emit error toast on failure', async () => {
515
- const toastHandler = vi.fn();
516
- runner.setToastHandler(toastHandler);
517
-
518
- await runner.execute({
519
- onClick: vi.fn().mockRejectedValue(new Error('fail')),
520
- errorMessage: 'Custom error',
521
- });
522
-
523
- expect(toastHandler).toHaveBeenCalledWith('Custom error', { type: 'error', duration: undefined });
524
- });
525
-
526
- it('should suppress toast when showOnSuccess is false', async () => {
527
- const toastHandler = vi.fn();
528
- runner.setToastHandler(toastHandler);
529
-
530
- await runner.execute({
531
- onClick: vi.fn(),
532
- toast: { showOnSuccess: false },
533
- });
534
-
535
- expect(toastHandler).not.toHaveBeenCalled();
536
- });
537
-
538
- it('should set reload when refreshAfter is true', async () => {
539
- const toastHandler = vi.fn();
540
- runner.setToastHandler(toastHandler);
541
-
542
- const result = await runner.execute({
543
- onClick: vi.fn(),
544
- refreshAfter: true,
545
- toast: { showOnSuccess: false },
546
- });
547
-
548
- expect(result.reload).toBe(true);
549
- });
550
- });
551
-
552
- // ==========================================================================
553
- // Action chaining
554
- // ==========================================================================
555
-
556
- describe('chaining', () => {
557
- it('should execute chained actions sequentially', async () => {
558
- const order: number[] = [];
559
- const handler1 = vi.fn(async () => { order.push(1); return { success: true }; });
560
- const handler2 = vi.fn(async () => { order.push(2); return { success: true }; });
561
- runner.registerHandler('step1', handler1);
562
- runner.registerHandler('step2', handler2);
563
-
564
- const result = await runner.execute({
565
- onClick: vi.fn(),
566
- chain: [
567
- { type: 'step1' },
568
- { type: 'step2' },
569
- ],
570
- });
571
-
572
- expect(result.success).toBe(true);
573
- expect(order).toEqual([1, 2]);
574
- });
575
-
576
- it('should stop sequential chain on failure', async () => {
577
- const handler1 = vi.fn().mockResolvedValue({ success: false, error: 'step1 fail' });
578
- const handler2 = vi.fn().mockResolvedValue({ success: true });
579
- runner.registerHandler('step1', handler1);
580
- runner.registerHandler('step2', handler2);
581
-
582
- const result = await runner.execute({
583
- onClick: vi.fn(),
584
- chain: [
585
- { type: 'step1' },
586
- { type: 'step2' },
587
- ],
588
- });
589
-
590
- expect(result.success).toBe(false);
591
- expect(result.error).toBe('step1 fail');
592
- expect(handler2).not.toHaveBeenCalled();
593
- });
594
-
595
- it('should execute chained actions in parallel', async () => {
596
- const handler1 = vi.fn().mockResolvedValue({ success: true });
597
- const handler2 = vi.fn().mockResolvedValue({ success: true });
598
- runner.registerHandler('a', handler1);
599
- runner.registerHandler('b', handler2);
600
-
601
- const result = await runner.execute({
602
- onClick: vi.fn(),
603
- chain: [{ type: 'a' }, { type: 'b' }],
604
- chainMode: 'parallel',
605
- });
606
-
607
- expect(result.success).toBe(true);
608
- expect(handler1).toHaveBeenCalledOnce();
609
- expect(handler2).toHaveBeenCalledOnce();
610
- });
611
- });
612
-
613
- // ==========================================================================
614
- // onSuccess / onFailure callbacks
615
- // ==========================================================================
616
-
617
- describe('callbacks', () => {
618
- it('should execute onSuccess callback after success', async () => {
619
- const successHandler = vi.fn().mockResolvedValue({ success: true });
620
- runner.registerHandler('notify', successHandler);
621
-
622
- await runner.execute({
623
- onClick: vi.fn(),
624
- onSuccess: { type: 'notify', params: { msg: 'ok' } },
625
- toast: { showOnSuccess: false },
626
- });
627
-
628
- expect(successHandler).toHaveBeenCalledOnce();
629
- });
630
-
631
- it('should execute onFailure callback after failure', async () => {
632
- const failureHandler = vi.fn().mockResolvedValue({ success: true });
633
- runner.registerHandler('log-error', failureHandler);
634
-
635
- await runner.execute({
636
- onClick: vi.fn().mockRejectedValue(new Error('fail')),
637
- onFailure: { type: 'log-error' },
638
- toast: { showOnError: false },
639
- });
640
-
641
- expect(failureHandler).toHaveBeenCalledOnce();
642
- });
643
-
644
- it('should support array of onSuccess callbacks', async () => {
645
- const h1 = vi.fn().mockResolvedValue({ success: true });
646
- const h2 = vi.fn().mockResolvedValue({ success: true });
647
- runner.registerHandler('cb1', h1);
648
- runner.registerHandler('cb2', h2);
649
-
650
- await runner.execute({
651
- onClick: vi.fn(),
652
- onSuccess: [{ type: 'cb1' }, { type: 'cb2' }],
653
- toast: { showOnSuccess: false },
654
- });
655
-
656
- expect(h1).toHaveBeenCalledOnce();
657
- expect(h2).toHaveBeenCalledOnce();
658
- });
659
- });
660
-
661
- // ==========================================================================
662
- // executeChain
663
- // ==========================================================================
664
-
665
- describe('executeChain', () => {
666
- it('should return success for empty chain', async () => {
667
- const result = await runner.executeChain([]);
668
- expect(result.success).toBe(true);
669
- });
670
-
671
- it('should execute single action chain', async () => {
672
- const onClick = vi.fn();
673
- const result = await runner.executeChain([{ onClick }]);
674
- expect(result.success).toBe(true);
675
- expect(onClick).toHaveBeenCalledOnce();
676
- });
677
- });
678
-
679
- // ==========================================================================
680
- // Context management
681
- // ==========================================================================
682
-
683
- describe('context', () => {
684
- it('should update context', () => {
685
- runner.updateContext({ record: { id: 2, status: 'closed' } });
686
- const ctx = runner.getContext();
687
- expect(ctx.record?.id).toBe(2);
688
- });
689
-
690
- it('should expose evaluator', () => {
691
- const ev = runner.getEvaluator();
692
- expect(ev).toBeDefined();
693
- expect(ev.evaluate('${data.name}')).toBe('Test');
694
- });
695
- });
696
-
697
- // ==========================================================================
698
- // executeAction convenience function
699
- // ==========================================================================
700
-
701
- describe('executeAction', () => {
702
- it('should execute an action with the convenience function', async () => {
703
- const result = await executeAction(
704
- { type: 'script', execute: '1 + 2' },
705
- { data: {} },
706
- );
707
- expect(result.success).toBe(true);
708
- expect(result.data).toBe(3);
709
- });
710
- });
711
- });