@startsimpli/funnels 0.1.3 → 0.1.5

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 (151) hide show
  1. package/package.json +9 -31
  2. package/src/api/README.md +507 -0
  3. package/src/api/adapter.ts +106 -0
  4. package/src/api/client.test.ts +640 -0
  5. package/src/api/client.ts +385 -0
  6. package/src/api/default-adapter.ts +243 -0
  7. package/src/api/index.ts +24 -0
  8. package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
  9. package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
  10. package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
  11. package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
  12. package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
  13. package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
  14. package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
  15. package/src/components/FilterRuleEditor/README.md +291 -0
  16. package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
  17. package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
  18. package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
  19. package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
  20. package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
  21. package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
  22. package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
  23. package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
  24. package/src/components/FilterRuleEditor/constants.ts +64 -0
  25. package/src/components/FilterRuleEditor/index.ts +14 -0
  26. package/src/components/FunnelCard/DESIGN.md +447 -0
  27. package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
  28. package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
  29. package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
  30. package/src/components/FunnelCard/FunnelCard.tsx +204 -0
  31. package/src/components/FunnelCard/FunnelStats.tsx +68 -0
  32. package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
  33. package/src/components/FunnelCard/INSTALLATION.md +304 -0
  34. package/src/components/FunnelCard/MatchBar.tsx +49 -0
  35. package/src/components/FunnelCard/README.md +294 -0
  36. package/src/components/FunnelCard/StageIndicator.tsx +62 -0
  37. package/src/components/FunnelCard/StatusBadge.tsx +52 -0
  38. package/src/components/FunnelCard/index.ts +14 -0
  39. package/src/components/FunnelPreview/EntityCard.tsx +72 -0
  40. package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
  41. package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
  42. package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
  43. package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
  44. package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
  45. package/src/components/FunnelPreview/README.md +337 -0
  46. package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
  47. package/src/components/FunnelPreview/example.tsx +286 -0
  48. package/src/components/FunnelPreview/index.ts +14 -0
  49. package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
  50. package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
  51. package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
  52. package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
  53. package/src/components/FunnelRunHistory/README.md +325 -0
  54. package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
  55. package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
  56. package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
  57. package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
  58. package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
  59. package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
  60. package/src/components/FunnelRunHistory/index.ts +51 -0
  61. package/src/components/FunnelRunHistory/types.ts +40 -0
  62. package/src/components/FunnelRunHistory/utils.test.ts +126 -0
  63. package/src/components/FunnelRunHistory/utils.ts +100 -0
  64. package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
  65. package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
  66. package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
  67. package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
  68. package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
  69. package/src/components/FunnelStageBuilder/README.md +341 -0
  70. package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
  71. package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
  72. package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
  73. package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
  74. package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
  75. package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
  76. package/src/components/FunnelStageBuilder/index.ts +21 -0
  77. package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
  78. package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
  79. package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
  80. package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
  81. package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
  82. package/src/components/FunnelVisualFlow/README.md +323 -0
  83. package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
  84. package/src/components/FunnelVisualFlow/example.tsx +227 -0
  85. package/src/components/FunnelVisualFlow/index.ts +10 -0
  86. package/src/components/index.ts +102 -0
  87. package/src/core/README.md +307 -0
  88. package/src/core/engine.test.ts +1087 -0
  89. package/src/core/engine.ts +329 -0
  90. package/src/core/evaluator.example.ts +353 -0
  91. package/src/core/evaluator.test.ts +639 -0
  92. package/src/core/evaluator.ts +261 -0
  93. package/src/core/field-resolver.example.ts +175 -0
  94. package/src/core/field-resolver.test.ts +541 -0
  95. package/src/core/field-resolver.ts +247 -0
  96. package/src/core/index.ts +34 -0
  97. package/src/core/operators.test.ts +539 -0
  98. package/src/core/operators.ts +241 -0
  99. package/src/hooks/index.ts +5 -0
  100. package/src/hooks/useDebouncedValue.ts +28 -0
  101. package/src/index.ts +155 -0
  102. package/src/store/README.md +342 -0
  103. package/src/store/create-funnel-store.test.ts +686 -0
  104. package/src/store/create-funnel-store.ts +538 -0
  105. package/src/store/index.ts +9 -0
  106. package/src/store/types.ts +294 -0
  107. package/src/stories/CrossDomain.stories.tsx +149 -0
  108. package/src/stories/Welcome.stories.tsx +81 -0
  109. package/src/stories/demo-data/index.ts +3 -0
  110. package/src/stories/demo-data/investors.ts +216 -0
  111. package/src/stories/demo-data/leads.ts +223 -0
  112. package/src/stories/demo-data/recipes.ts +217 -0
  113. package/src/test/setup.ts +5 -0
  114. package/src/types/index.ts +843 -0
  115. package/dist/client-3ESO2NHy.d.ts +0 -310
  116. package/dist/client-CZu03ACp.d.cts +0 -310
  117. package/dist/components/index.cjs +0 -3243
  118. package/dist/components/index.cjs.map +0 -1
  119. package/dist/components/index.css.map +0 -1
  120. package/dist/components/index.d.cts +0 -726
  121. package/dist/components/index.d.ts +0 -726
  122. package/dist/components/index.js +0 -3196
  123. package/dist/components/index.js.map +0 -1
  124. package/dist/core/index.cjs +0 -500
  125. package/dist/core/index.cjs.map +0 -1
  126. package/dist/core/index.d.cts +0 -359
  127. package/dist/core/index.d.ts +0 -359
  128. package/dist/core/index.js +0 -486
  129. package/dist/core/index.js.map +0 -1
  130. package/dist/hooks/index.cjs +0 -21
  131. package/dist/hooks/index.cjs.map +0 -1
  132. package/dist/hooks/index.d.cts +0 -11
  133. package/dist/hooks/index.d.ts +0 -11
  134. package/dist/hooks/index.js +0 -19
  135. package/dist/hooks/index.js.map +0 -1
  136. package/dist/index-BGDEXbuz.d.cts +0 -434
  137. package/dist/index-BGDEXbuz.d.ts +0 -434
  138. package/dist/index.cjs +0 -4499
  139. package/dist/index.cjs.map +0 -1
  140. package/dist/index.css +0 -198
  141. package/dist/index.css.map +0 -1
  142. package/dist/index.d.cts +0 -99
  143. package/dist/index.d.ts +0 -99
  144. package/dist/index.js +0 -4421
  145. package/dist/index.js.map +0 -1
  146. package/dist/store/index.cjs +0 -391
  147. package/dist/store/index.cjs.map +0 -1
  148. package/dist/store/index.d.cts +0 -225
  149. package/dist/store/index.d.ts +0 -225
  150. package/dist/store/index.js +0 -388
  151. package/dist/store/index.js.map +0 -1
@@ -0,0 +1,520 @@
1
+ /**
2
+ * FilterRuleEditor Component Tests
3
+ *
4
+ * Tests the BRUTALLY GENERIC filter rule builder with different entity types.
5
+ */
6
+
7
+ import { describe, it, expect, vi } from 'vitest';
8
+ import { FilterRule, FieldDefinition, FilterLogic } from '../../types';
9
+ import { validateFilterRule } from '../../types';
10
+
11
+ // ============================================================================
12
+ // Mock Field Registries - Shows genericness!
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Investor field registry
17
+ */
18
+ const investorFieldRegistry: FieldDefinition[] = [
19
+ {
20
+ name: 'name',
21
+ label: 'Name',
22
+ type: 'string',
23
+ category: 'Profile',
24
+ operators: ['eq', 'ne', 'contains', 'startswith'],
25
+ },
26
+ {
27
+ name: 'email',
28
+ label: 'Email',
29
+ type: 'string',
30
+ category: 'Profile',
31
+ operators: ['eq', 'contains', 'isnotnull'],
32
+ },
33
+ {
34
+ name: 'firm.stage',
35
+ label: 'Firm Stage',
36
+ type: 'string',
37
+ category: 'Investment',
38
+ operators: ['eq', 'ne', 'in'],
39
+ constraints: {
40
+ choices: ['Seed', 'Series A', 'Series B', 'Series C', 'Growth'],
41
+ },
42
+ },
43
+ {
44
+ name: 'firm.check_size_min',
45
+ label: 'Check Size Min',
46
+ type: 'number',
47
+ category: 'Investment',
48
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
49
+ constraints: {
50
+ min_value: 0,
51
+ },
52
+ },
53
+ {
54
+ name: 'metrics.icp_score',
55
+ label: 'ICP Score',
56
+ type: 'number',
57
+ category: 'Metrics',
58
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
59
+ constraints: {
60
+ min_value: 0,
61
+ max_value: 100,
62
+ },
63
+ },
64
+ {
65
+ name: 'tags',
66
+ label: 'Tags',
67
+ type: 'tag',
68
+ category: 'Metadata',
69
+ operators: ['has_tag', 'not_has_tag'],
70
+ },
71
+ ];
72
+
73
+ /**
74
+ * Recipe field registry - completely different domain!
75
+ */
76
+ const recipeFieldRegistry: FieldDefinition[] = [
77
+ {
78
+ name: 'title',
79
+ label: 'Recipe Title',
80
+ type: 'string',
81
+ category: 'Basic Info',
82
+ operators: ['eq', 'contains', 'startswith'],
83
+ },
84
+ {
85
+ name: 'cuisine',
86
+ label: 'Cuisine',
87
+ type: 'string',
88
+ category: 'Classification',
89
+ operators: ['eq', 'in'],
90
+ constraints: {
91
+ choices: ['Italian', 'French', 'Chinese', 'Japanese', 'Mexican', 'Indian'],
92
+ },
93
+ },
94
+ {
95
+ name: 'difficulty',
96
+ label: 'Difficulty Level',
97
+ type: 'string',
98
+ category: 'Classification',
99
+ operators: ['eq', 'in'],
100
+ constraints: {
101
+ choices: ['Easy', 'Medium', 'Hard'],
102
+ },
103
+ },
104
+ {
105
+ name: 'prep_time_minutes',
106
+ label: 'Prep Time (min)',
107
+ type: 'number',
108
+ category: 'Time',
109
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
110
+ constraints: {
111
+ min_value: 0,
112
+ },
113
+ },
114
+ {
115
+ name: 'is_vegetarian',
116
+ label: 'Vegetarian',
117
+ type: 'boolean',
118
+ category: 'Dietary',
119
+ operators: ['eq', 'is_true', 'is_false'],
120
+ },
121
+ {
122
+ name: 'created_at',
123
+ label: 'Created Date',
124
+ type: 'date',
125
+ category: 'Metadata',
126
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
127
+ },
128
+ ];
129
+
130
+ // ============================================================================
131
+ // Tests
132
+ // ============================================================================
133
+
134
+ describe('FilterRuleEditor - Generic Validation', () => {
135
+ it('validates empty field_path', () => {
136
+ const rule: FilterRule = {
137
+ field_path: '',
138
+ operator: 'eq',
139
+ value: 'test',
140
+ };
141
+
142
+ const errors = validateFilterRule(rule);
143
+ expect(errors).toContain('field_path is required');
144
+ });
145
+
146
+ it('validates missing operator', () => {
147
+ const rule: FilterRule = {
148
+ field_path: 'name',
149
+ operator: '' as any,
150
+ value: 'test',
151
+ };
152
+
153
+ const errors = validateFilterRule(rule);
154
+ expect(errors).toContain('operator is required');
155
+ });
156
+
157
+ it('validates missing value for operators that need it', () => {
158
+ const rule: FilterRule = {
159
+ field_path: 'name',
160
+ operator: 'eq',
161
+ value: undefined,
162
+ };
163
+
164
+ const errors = validateFilterRule(rule);
165
+ expect(errors.length).toBeGreaterThan(0);
166
+ expect(errors[0]).toContain('value is required');
167
+ });
168
+
169
+ it('allows null value for null-check operators', () => {
170
+ const nullOps = ['isnull', 'isnotnull', 'is_true', 'is_false'];
171
+
172
+ nullOps.forEach((op) => {
173
+ const rule: FilterRule = {
174
+ field_path: 'email',
175
+ operator: op as any,
176
+ value: null,
177
+ };
178
+
179
+ const errors = validateFilterRule(rule);
180
+ expect(errors).toEqual([]);
181
+ });
182
+ });
183
+ });
184
+
185
+ describe('FilterRuleEditor - Investor Domain', () => {
186
+ it('creates string equality rule', () => {
187
+ const rule: FilterRule = {
188
+ field_path: 'name',
189
+ operator: 'eq',
190
+ value: 'John Doe',
191
+ };
192
+
193
+ expect(validateFilterRule(rule)).toEqual([]);
194
+ });
195
+
196
+ it('creates string contains rule', () => {
197
+ const rule: FilterRule = {
198
+ field_path: 'email',
199
+ operator: 'contains',
200
+ value: '@vc.com',
201
+ };
202
+
203
+ expect(validateFilterRule(rule)).toEqual([]);
204
+ });
205
+
206
+ it('creates choice equality rule', () => {
207
+ const rule: FilterRule = {
208
+ field_path: 'firm.stage',
209
+ operator: 'eq',
210
+ value: 'Series A',
211
+ };
212
+
213
+ expect(validateFilterRule(rule)).toEqual([]);
214
+ });
215
+
216
+ it('creates choice multi-select rule', () => {
217
+ const rule: FilterRule = {
218
+ field_path: 'firm.stage',
219
+ operator: 'in',
220
+ value: ['Series A', 'Series B', 'Series C'],
221
+ };
222
+
223
+ expect(validateFilterRule(rule)).toEqual([]);
224
+ });
225
+
226
+ it('creates number comparison rule', () => {
227
+ const rule: FilterRule = {
228
+ field_path: 'firm.check_size_min',
229
+ operator: 'gte',
230
+ value: 1000000,
231
+ };
232
+
233
+ expect(validateFilterRule(rule)).toEqual([]);
234
+ });
235
+
236
+ it('creates number range rule', () => {
237
+ const rule: FilterRule = {
238
+ field_path: 'metrics.icp_score',
239
+ operator: 'gte',
240
+ value: 80,
241
+ };
242
+
243
+ expect(validateFilterRule(rule)).toEqual([]);
244
+ });
245
+
246
+ it('creates tag rule', () => {
247
+ const rule: FilterRule = {
248
+ field_path: 'tags',
249
+ operator: 'has_tag',
250
+ value: 'priority',
251
+ };
252
+
253
+ expect(validateFilterRule(rule)).toEqual([]);
254
+ });
255
+ });
256
+
257
+ describe('FilterRuleEditor - Recipe Domain', () => {
258
+ it('creates recipe title search rule', () => {
259
+ const rule: FilterRule = {
260
+ field_path: 'title',
261
+ operator: 'contains',
262
+ value: 'pasta',
263
+ };
264
+
265
+ expect(validateFilterRule(rule)).toEqual([]);
266
+ });
267
+
268
+ it('creates cuisine filter rule', () => {
269
+ const rule: FilterRule = {
270
+ field_path: 'cuisine',
271
+ operator: 'eq',
272
+ value: 'Italian',
273
+ };
274
+
275
+ expect(validateFilterRule(rule)).toEqual([]);
276
+ });
277
+
278
+ it('creates multiple cuisine rule', () => {
279
+ const rule: FilterRule = {
280
+ field_path: 'cuisine',
281
+ operator: 'in',
282
+ value: ['Italian', 'French', 'Japanese'],
283
+ };
284
+
285
+ expect(validateFilterRule(rule)).toEqual([]);
286
+ });
287
+
288
+ it('creates difficulty filter rule', () => {
289
+ const rule: FilterRule = {
290
+ field_path: 'difficulty',
291
+ operator: 'eq',
292
+ value: 'Easy',
293
+ };
294
+
295
+ expect(validateFilterRule(rule)).toEqual([]);
296
+ });
297
+
298
+ it('creates prep time rule', () => {
299
+ const rule: FilterRule = {
300
+ field_path: 'prep_time_minutes',
301
+ operator: 'lte',
302
+ value: 30,
303
+ };
304
+
305
+ expect(validateFilterRule(rule)).toEqual([]);
306
+ });
307
+
308
+ it('creates boolean rule', () => {
309
+ const rule: FilterRule = {
310
+ field_path: 'is_vegetarian',
311
+ operator: 'is_true',
312
+ value: null,
313
+ };
314
+
315
+ expect(validateFilterRule(rule)).toEqual([]);
316
+ });
317
+
318
+ it('creates date comparison rule', () => {
319
+ const rule: FilterRule = {
320
+ field_path: 'created_at',
321
+ operator: 'gte',
322
+ value: '2024-01-01',
323
+ };
324
+
325
+ expect(validateFilterRule(rule)).toEqual([]);
326
+ });
327
+ });
328
+
329
+ describe('FilterRuleEditor - Complex Multi-Rule Scenarios', () => {
330
+ it('validates AND logic with multiple rules', () => {
331
+ const rules: FilterRule[] = [
332
+ {
333
+ field_path: 'firm.stage',
334
+ operator: 'in',
335
+ value: ['Series A', 'Series B'],
336
+ },
337
+ {
338
+ field_path: 'firm.check_size_min',
339
+ operator: 'gte',
340
+ value: 500000,
341
+ },
342
+ {
343
+ field_path: 'metrics.icp_score',
344
+ operator: 'gte',
345
+ value: 70,
346
+ },
347
+ ];
348
+
349
+ rules.forEach((rule) => {
350
+ expect(validateFilterRule(rule)).toEqual([]);
351
+ });
352
+ });
353
+
354
+ it('validates OR logic with different field types', () => {
355
+ const rules: FilterRule[] = [
356
+ {
357
+ field_path: 'name',
358
+ operator: 'contains',
359
+ value: 'Acme',
360
+ },
361
+ {
362
+ field_path: 'tags',
363
+ operator: 'has_tag',
364
+ value: 'hot-lead',
365
+ },
366
+ {
367
+ field_path: 'metrics.icp_score',
368
+ operator: 'gte',
369
+ value: 90,
370
+ },
371
+ ];
372
+
373
+ rules.forEach((rule) => {
374
+ expect(validateFilterRule(rule)).toEqual([]);
375
+ });
376
+ });
377
+
378
+ it('validates recipe filter with multiple conditions', () => {
379
+ const rules: FilterRule[] = [
380
+ {
381
+ field_path: 'cuisine',
382
+ operator: 'in',
383
+ value: ['Italian', 'French'],
384
+ },
385
+ {
386
+ field_path: 'difficulty',
387
+ operator: 'eq',
388
+ value: 'Easy',
389
+ },
390
+ {
391
+ field_path: 'prep_time_minutes',
392
+ operator: 'lte',
393
+ value: 30,
394
+ },
395
+ {
396
+ field_path: 'is_vegetarian',
397
+ operator: 'is_true',
398
+ value: null,
399
+ },
400
+ ];
401
+
402
+ rules.forEach((rule) => {
403
+ expect(validateFilterRule(rule)).toEqual([]);
404
+ });
405
+ });
406
+ });
407
+
408
+ describe('FilterRuleEditor - Field Registry Grouping', () => {
409
+ it('groups investor fields by category', () => {
410
+ const grouped = investorFieldRegistry.reduce((acc, field) => {
411
+ const category = field.category || 'Other';
412
+ if (!acc[category]) {
413
+ acc[category] = [];
414
+ }
415
+ acc[category].push(field);
416
+ return acc;
417
+ }, {} as Record<string, FieldDefinition[]>);
418
+
419
+ expect(Object.keys(grouped)).toContain('Profile');
420
+ expect(Object.keys(grouped)).toContain('Investment');
421
+ expect(Object.keys(grouped)).toContain('Metrics');
422
+ expect(grouped['Profile'].length).toBe(2);
423
+ expect(grouped['Investment'].length).toBe(2);
424
+ });
425
+
426
+ it('groups recipe fields by category', () => {
427
+ const grouped = recipeFieldRegistry.reduce((acc, field) => {
428
+ const category = field.category || 'Other';
429
+ if (!acc[category]) {
430
+ acc[category] = [];
431
+ }
432
+ acc[category].push(field);
433
+ return acc;
434
+ }, {} as Record<string, FieldDefinition[]>);
435
+
436
+ expect(Object.keys(grouped)).toContain('Basic Info');
437
+ expect(Object.keys(grouped)).toContain('Classification');
438
+ expect(Object.keys(grouped)).toContain('Time');
439
+ expect(Object.keys(grouped)).toContain('Dietary');
440
+ });
441
+ });
442
+
443
+ describe('FilterRuleEditor - Operator Filtering', () => {
444
+ it('provides correct operators for string fields', () => {
445
+ const nameField = investorFieldRegistry.find((f) => f.name === 'name');
446
+ expect(nameField?.operators).toContain('eq');
447
+ expect(nameField?.operators).toContain('contains');
448
+ expect(nameField?.operators).toContain('startswith');
449
+ });
450
+
451
+ it('provides correct operators for number fields', () => {
452
+ const checkSizeField = investorFieldRegistry.find(
453
+ (f) => f.name === 'firm.check_size_min'
454
+ );
455
+ expect(checkSizeField?.operators).toContain('gt');
456
+ expect(checkSizeField?.operators).toContain('gte');
457
+ expect(checkSizeField?.operators).toContain('lte');
458
+ });
459
+
460
+ it('provides correct operators for choice fields', () => {
461
+ const stageField = investorFieldRegistry.find((f) => f.name === 'firm.stage');
462
+ expect(stageField?.operators).toContain('eq');
463
+ expect(stageField?.operators).toContain('in');
464
+ });
465
+
466
+ it('provides correct operators for tag fields', () => {
467
+ const tagsField = investorFieldRegistry.find((f) => f.name === 'tags');
468
+ expect(tagsField?.operators).toContain('has_tag');
469
+ expect(tagsField?.operators).toContain('not_has_tag');
470
+ });
471
+
472
+ it('provides correct operators for boolean fields', () => {
473
+ const vegField = recipeFieldRegistry.find((f) => f.name === 'is_vegetarian');
474
+ expect(vegField?.operators).toContain('eq');
475
+ expect(vegField?.operators).toContain('is_true');
476
+ expect(vegField?.operators).toContain('is_false');
477
+ });
478
+
479
+ it('provides correct operators for date fields', () => {
480
+ const dateField = recipeFieldRegistry.find((f) => f.name === 'created_at');
481
+ expect(dateField?.operators).toContain('eq');
482
+ expect(dateField?.operators).toContain('gt');
483
+ expect(dateField?.operators).toContain('lte');
484
+ });
485
+ });
486
+
487
+ describe('FilterRuleEditor - Value Constraints', () => {
488
+ it('validates choice constraints for firm stage', () => {
489
+ const stageField = investorFieldRegistry.find((f) => f.name === 'firm.stage');
490
+ expect(stageField?.constraints?.choices).toEqual([
491
+ 'Seed',
492
+ 'Series A',
493
+ 'Series B',
494
+ 'Series C',
495
+ 'Growth',
496
+ ]);
497
+ });
498
+
499
+ it('validates choice constraints for cuisine', () => {
500
+ const cuisineField = recipeFieldRegistry.find((f) => f.name === 'cuisine');
501
+ expect(cuisineField?.constraints?.choices).toContain('Italian');
502
+ expect(cuisineField?.constraints?.choices).toContain('French');
503
+ expect(cuisineField?.constraints?.choices).toContain('Japanese');
504
+ });
505
+
506
+ it('validates number constraints for ICP score', () => {
507
+ const scoreField = investorFieldRegistry.find(
508
+ (f) => f.name === 'metrics.icp_score'
509
+ );
510
+ expect(scoreField?.constraints?.min_value).toBe(0);
511
+ expect(scoreField?.constraints?.max_value).toBe(100);
512
+ });
513
+
514
+ it('validates number constraints for prep time', () => {
515
+ const prepField = recipeFieldRegistry.find(
516
+ (f) => f.name === 'prep_time_minutes'
517
+ );
518
+ expect(prepField?.constraints?.min_value).toBe(0);
519
+ });
520
+ });