@startsimpli/funnels 0.1.4 → 0.1.6
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/package.json +9 -31
- package/src/api/README.md +507 -0
- package/src/api/adapter.ts +106 -0
- package/src/api/client.test.ts +640 -0
- package/src/api/client.ts +385 -0
- package/src/api/default-adapter.ts +243 -0
- package/src/api/index.ts +24 -0
- package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
- package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
- package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
- package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
- package/src/components/FilterRuleEditor/README.md +291 -0
- package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
- package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
- package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
- package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
- package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
- package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
- package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
- package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
- package/src/components/FilterRuleEditor/constants.ts +64 -0
- package/src/components/FilterRuleEditor/index.ts +14 -0
- package/src/components/FunnelCard/DESIGN.md +447 -0
- package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
- package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
- package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
- package/src/components/FunnelCard/FunnelCard.tsx +204 -0
- package/src/components/FunnelCard/FunnelStats.tsx +68 -0
- package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
- package/src/components/FunnelCard/INSTALLATION.md +304 -0
- package/src/components/FunnelCard/MatchBar.tsx +49 -0
- package/src/components/FunnelCard/README.md +294 -0
- package/src/components/FunnelCard/StageIndicator.tsx +62 -0
- package/src/components/FunnelCard/StatusBadge.tsx +52 -0
- package/src/components/FunnelCard/index.ts +14 -0
- package/src/components/FunnelPreview/EntityCard.tsx +72 -0
- package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
- package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
- package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
- package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
- package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
- package/src/components/FunnelPreview/README.md +337 -0
- package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
- package/src/components/FunnelPreview/example.tsx +286 -0
- package/src/components/FunnelPreview/index.ts +14 -0
- package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
- package/src/components/FunnelRunHistory/README.md +325 -0
- package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
- package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
- package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
- package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
- package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
- package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
- package/src/components/FunnelRunHistory/index.ts +51 -0
- package/src/components/FunnelRunHistory/types.ts +40 -0
- package/src/components/FunnelRunHistory/utils.test.ts +126 -0
- package/src/components/FunnelRunHistory/utils.ts +100 -0
- package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
- package/src/components/FunnelStageBuilder/README.md +341 -0
- package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
- package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
- package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
- package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
- package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
- package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
- package/src/components/FunnelStageBuilder/index.ts +21 -0
- package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
- package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
- package/src/components/FunnelVisualFlow/README.md +323 -0
- package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
- package/src/components/FunnelVisualFlow/example.tsx +227 -0
- package/src/components/FunnelVisualFlow/index.ts +10 -0
- package/src/components/index.ts +102 -0
- package/src/core/README.md +307 -0
- package/src/core/engine.test.ts +1087 -0
- package/src/core/engine.ts +329 -0
- package/src/core/evaluator.example.ts +353 -0
- package/src/core/evaluator.test.ts +639 -0
- package/src/core/evaluator.ts +261 -0
- package/src/core/field-resolver.example.ts +175 -0
- package/src/core/field-resolver.test.ts +541 -0
- package/src/core/field-resolver.ts +247 -0
- package/src/core/index.ts +34 -0
- package/src/core/operators.test.ts +539 -0
- package/src/core/operators.ts +241 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useDebouncedValue.ts +28 -0
- package/src/index.ts +155 -0
- package/src/store/README.md +342 -0
- package/src/store/create-funnel-store.test.ts +686 -0
- package/src/store/create-funnel-store.ts +538 -0
- package/src/store/index.ts +9 -0
- package/src/store/types.ts +294 -0
- package/src/stories/CrossDomain.stories.tsx +149 -0
- package/src/stories/Welcome.stories.tsx +81 -0
- package/src/stories/demo-data/index.ts +3 -0
- package/src/stories/demo-data/investors.ts +216 -0
- package/src/stories/demo-data/leads.ts +223 -0
- package/src/stories/demo-data/recipes.ts +217 -0
- package/src/test/setup.ts +5 -0
- package/src/types/index.ts +843 -0
- package/dist/client-3ESO2NHy.d.ts +0 -310
- package/dist/client-CZu03ACp.d.cts +0 -310
- package/dist/components/index.cjs +0 -3241
- package/dist/components/index.cjs.map +0 -1
- package/dist/components/index.css.map +0 -1
- package/dist/components/index.d.cts +0 -726
- package/dist/components/index.d.ts +0 -726
- package/dist/components/index.js +0 -3194
- package/dist/components/index.js.map +0 -1
- package/dist/core/index.cjs +0 -500
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -359
- package/dist/core/index.d.ts +0 -359
- package/dist/core/index.js +0 -486
- package/dist/core/index.js.map +0 -1
- package/dist/hooks/index.cjs +0 -20
- package/dist/hooks/index.cjs.map +0 -1
- package/dist/hooks/index.d.cts +0 -11
- package/dist/hooks/index.d.ts +0 -11
- package/dist/hooks/index.js +0 -18
- package/dist/hooks/index.js.map +0 -1
- package/dist/index-BGDEXbuz.d.cts +0 -434
- package/dist/index-BGDEXbuz.d.ts +0 -434
- package/dist/index.cjs +0 -4499
- package/dist/index.cjs.map +0 -1
- package/dist/index.css +0 -198
- package/dist/index.css.map +0 -1
- package/dist/index.d.cts +0 -99
- package/dist/index.d.ts +0 -99
- package/dist/index.js +0 -4421
- package/dist/index.js.map +0 -1
- package/dist/store/index.cjs +0 -389
- package/dist/store/index.cjs.map +0 -1
- package/dist/store/index.d.cts +0 -225
- package/dist/store/index.d.ts +0 -225
- package/dist/store/index.js +0 -386
- package/dist/store/index.js.map +0 -1
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @startsimpli/funnels - Operator Tests
|
|
3
|
+
*
|
|
4
|
+
* Test suite for operator implementations.
|
|
5
|
+
* Tests each operator in isolation with various data types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { applyOperator } from './operators';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Equality Operators
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
describe('Equality Operators', () => {
|
|
16
|
+
describe('eq', () => {
|
|
17
|
+
it('should match equal strings', () => {
|
|
18
|
+
expect(applyOperator('eq', 'hello', 'hello')).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should not match different strings', () => {
|
|
22
|
+
expect(applyOperator('eq', 'hello', 'world')).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should match equal numbers', () => {
|
|
26
|
+
expect(applyOperator('eq', 42, 42)).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should not match different numbers', () => {
|
|
30
|
+
expect(applyOperator('eq', 42, 43)).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should match null with null', () => {
|
|
34
|
+
expect(applyOperator('eq', null, null)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should match undefined with undefined', () => {
|
|
38
|
+
expect(applyOperator('eq', undefined, undefined)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should not match null with value', () => {
|
|
42
|
+
expect(applyOperator('eq', null, 'value')).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('ne', () => {
|
|
47
|
+
it('should match different strings', () => {
|
|
48
|
+
expect(applyOperator('ne', 'hello', 'world')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should not match equal strings', () => {
|
|
52
|
+
expect(applyOperator('ne', 'hello', 'hello')).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should match different numbers', () => {
|
|
56
|
+
expect(applyOperator('ne', 42, 43)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should not match equal numbers', () => {
|
|
60
|
+
expect(applyOperator('ne', 42, 42)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Comparison Operators
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
describe('Comparison Operators', () => {
|
|
70
|
+
describe('gt', () => {
|
|
71
|
+
it('should match greater numbers', () => {
|
|
72
|
+
expect(applyOperator('gt', 10, 5)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should not match equal numbers', () => {
|
|
76
|
+
expect(applyOperator('gt', 5, 5)).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should not match lesser numbers', () => {
|
|
80
|
+
expect(applyOperator('gt', 5, 10)).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should handle decimal numbers', () => {
|
|
84
|
+
expect(applyOperator('gt', 5.5, 5.4)).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should handle numeric strings', () => {
|
|
88
|
+
expect(applyOperator('gt', '10', '5')).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should compare dates', () => {
|
|
92
|
+
expect(applyOperator('gt', new Date('2024-02-01'), new Date('2024-01-01'))).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('lt', () => {
|
|
97
|
+
it('should match lesser numbers', () => {
|
|
98
|
+
expect(applyOperator('lt', 5, 10)).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should not match equal numbers', () => {
|
|
102
|
+
expect(applyOperator('lt', 5, 5)).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should not match greater numbers', () => {
|
|
106
|
+
expect(applyOperator('lt', 10, 5)).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('gte', () => {
|
|
111
|
+
it('should match greater numbers', () => {
|
|
112
|
+
expect(applyOperator('gte', 10, 5)).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should match equal numbers', () => {
|
|
116
|
+
expect(applyOperator('gte', 5, 5)).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should not match lesser numbers', () => {
|
|
120
|
+
expect(applyOperator('gte', 5, 10)).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('lte', () => {
|
|
125
|
+
it('should match lesser numbers', () => {
|
|
126
|
+
expect(applyOperator('lte', 5, 10)).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should match equal numbers', () => {
|
|
130
|
+
expect(applyOperator('lte', 5, 5)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should not match greater numbers', () => {
|
|
134
|
+
expect(applyOperator('lte', 10, 5)).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// String Operators
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
describe('String Operators', () => {
|
|
144
|
+
describe('contains', () => {
|
|
145
|
+
it('should match substring (case-insensitive)', () => {
|
|
146
|
+
expect(applyOperator('contains', 'Hello World', 'world')).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should not match non-existent substring', () => {
|
|
150
|
+
expect(applyOperator('contains', 'Hello World', 'foo')).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle empty substring', () => {
|
|
154
|
+
expect(applyOperator('contains', 'Hello', '')).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should return false for null actual value', () => {
|
|
158
|
+
expect(applyOperator('contains', null, 'test')).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should return false for null expected value', () => {
|
|
162
|
+
expect(applyOperator('contains', 'test', null)).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('not_contains', () => {
|
|
167
|
+
it('should match when substring absent', () => {
|
|
168
|
+
expect(applyOperator('not_contains', 'Hello World', 'foo')).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should not match when substring present', () => {
|
|
172
|
+
expect(applyOperator('not_contains', 'Hello World', 'world')).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should return true for null actual value', () => {
|
|
176
|
+
expect(applyOperator('not_contains', null, 'test')).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('startswith', () => {
|
|
181
|
+
it('should match prefix (case-insensitive)', () => {
|
|
182
|
+
expect(applyOperator('startswith', 'Hello World', 'hello')).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should not match non-prefix', () => {
|
|
186
|
+
expect(applyOperator('startswith', 'Hello World', 'world')).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should match empty string', () => {
|
|
190
|
+
expect(applyOperator('startswith', 'Hello', '')).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should return false for null', () => {
|
|
194
|
+
expect(applyOperator('startswith', null, 'test')).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('endswith', () => {
|
|
199
|
+
it('should match suffix (case-insensitive)', () => {
|
|
200
|
+
expect(applyOperator('endswith', 'Hello World', 'world')).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should not match non-suffix', () => {
|
|
204
|
+
expect(applyOperator('endswith', 'Hello World', 'hello')).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should match empty string', () => {
|
|
208
|
+
expect(applyOperator('endswith', 'Hello', '')).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should return false for null', () => {
|
|
212
|
+
expect(applyOperator('endswith', null, 'test')).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('matches', () => {
|
|
217
|
+
it('should match valid regex', () => {
|
|
218
|
+
expect(applyOperator('matches', 'test@example.com', '^[a-z]+@[a-z]+\\.[a-z]+$')).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should not match invalid regex', () => {
|
|
222
|
+
expect(applyOperator('matches', 'test', '^[0-9]+$')).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should handle regex with flags', () => {
|
|
226
|
+
expect(applyOperator('matches', 'Test123', '[a-z]+[0-9]+')).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should return false for invalid regex pattern', () => {
|
|
230
|
+
expect(applyOperator('matches', 'test', '[invalid')).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should return false for null', () => {
|
|
234
|
+
expect(applyOperator('matches', null, 'test')).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ============================================================================
|
|
240
|
+
// Array Operators
|
|
241
|
+
// ============================================================================
|
|
242
|
+
|
|
243
|
+
describe('Array Operators', () => {
|
|
244
|
+
describe('in', () => {
|
|
245
|
+
it('should match value in array', () => {
|
|
246
|
+
expect(applyOperator('in', 'apple', ['apple', 'banana', 'cherry'])).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should not match value not in array', () => {
|
|
250
|
+
expect(applyOperator('in', 'orange', ['apple', 'banana', 'cherry'])).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should match number in array', () => {
|
|
254
|
+
expect(applyOperator('in', 2, [1, 2, 3])).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should return false for non-array expected', () => {
|
|
258
|
+
expect(applyOperator('in', 'test', 'not-array')).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should return false for empty array', () => {
|
|
262
|
+
expect(applyOperator('in', 'test', [])).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('not_in', () => {
|
|
267
|
+
it('should match value not in array', () => {
|
|
268
|
+
expect(applyOperator('not_in', 'orange', ['apple', 'banana', 'cherry'])).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should not match value in array', () => {
|
|
272
|
+
expect(applyOperator('not_in', 'apple', ['apple', 'banana', 'cherry'])).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should return true for non-array expected', () => {
|
|
276
|
+
expect(applyOperator('not_in', 'test', 'not-array')).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('has_any', () => {
|
|
281
|
+
it('should match when array has any value', () => {
|
|
282
|
+
expect(applyOperator('has_any', ['a', 'b', 'c'], ['b', 'd'])).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should not match when array has no values', () => {
|
|
286
|
+
expect(applyOperator('has_any', ['a', 'b', 'c'], ['d', 'e'])).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should return false for non-array actual', () => {
|
|
290
|
+
expect(applyOperator('has_any', 'not-array', ['a', 'b'])).toBe(false);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should return false for non-array expected', () => {
|
|
294
|
+
expect(applyOperator('has_any', ['a', 'b'], 'not-array')).toBe(false);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should return false for empty arrays', () => {
|
|
298
|
+
expect(applyOperator('has_any', [], [])).toBe(false);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('has_all', () => {
|
|
303
|
+
it('should match when array has all values', () => {
|
|
304
|
+
expect(applyOperator('has_all', ['a', 'b', 'c'], ['a', 'b'])).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should not match when array missing values', () => {
|
|
308
|
+
expect(applyOperator('has_all', ['a', 'b', 'c'], ['a', 'd'])).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should match empty expected array', () => {
|
|
312
|
+
expect(applyOperator('has_all', ['a', 'b'], [])).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should return false for non-array actual', () => {
|
|
316
|
+
expect(applyOperator('has_all', 'not-array', ['a', 'b'])).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should return false for non-array expected', () => {
|
|
320
|
+
expect(applyOperator('has_all', ['a', 'b'], 'not-array')).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// ============================================================================
|
|
326
|
+
// Null Operators
|
|
327
|
+
// ============================================================================
|
|
328
|
+
|
|
329
|
+
describe('Null Operators', () => {
|
|
330
|
+
describe('isnull', () => {
|
|
331
|
+
it('should match null', () => {
|
|
332
|
+
expect(applyOperator('isnull', null, null)).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should match undefined', () => {
|
|
336
|
+
expect(applyOperator('isnull', undefined, null)).toBe(true);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should not match defined values', () => {
|
|
340
|
+
expect(applyOperator('isnull', 'value', null)).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should not match empty string', () => {
|
|
344
|
+
expect(applyOperator('isnull', '', null)).toBe(false);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should not match zero', () => {
|
|
348
|
+
expect(applyOperator('isnull', 0, null)).toBe(false);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should not match false', () => {
|
|
352
|
+
expect(applyOperator('isnull', false, null)).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe('isnotnull', () => {
|
|
357
|
+
it('should not match null', () => {
|
|
358
|
+
expect(applyOperator('isnotnull', null, null)).toBe(false);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should not match undefined', () => {
|
|
362
|
+
expect(applyOperator('isnotnull', undefined, null)).toBe(false);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should match defined values', () => {
|
|
366
|
+
expect(applyOperator('isnotnull', 'value', null)).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should match empty string', () => {
|
|
370
|
+
expect(applyOperator('isnotnull', '', null)).toBe(true);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should match zero', () => {
|
|
374
|
+
expect(applyOperator('isnotnull', 0, null)).toBe(true);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should match false', () => {
|
|
378
|
+
expect(applyOperator('isnotnull', false, null)).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// ============================================================================
|
|
384
|
+
// Tag Operators
|
|
385
|
+
// ============================================================================
|
|
386
|
+
|
|
387
|
+
describe('Tag Operators', () => {
|
|
388
|
+
describe('has_tag', () => {
|
|
389
|
+
it('should match tag in array (case-insensitive)', () => {
|
|
390
|
+
expect(applyOperator('has_tag', ['active', 'qualified'], 'QUALIFIED')).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should not match missing tag', () => {
|
|
394
|
+
expect(applyOperator('has_tag', ['active', 'qualified'], 'premium')).toBe(false);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should return false for non-array', () => {
|
|
398
|
+
expect(applyOperator('has_tag', 'not-array', 'tag')).toBe(false);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should return false for empty array', () => {
|
|
402
|
+
expect(applyOperator('has_tag', [], 'tag')).toBe(false);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should return false for null expected', () => {
|
|
406
|
+
expect(applyOperator('has_tag', ['active'], null)).toBe(false);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe('not_has_tag', () => {
|
|
411
|
+
it('should match when tag absent', () => {
|
|
412
|
+
expect(applyOperator('not_has_tag', ['active', 'qualified'], 'premium')).toBe(true);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should not match when tag present', () => {
|
|
416
|
+
expect(applyOperator('not_has_tag', ['active', 'qualified'], 'ACTIVE')).toBe(false);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should return true for non-array', () => {
|
|
420
|
+
expect(applyOperator('not_has_tag', 'not-array', 'tag')).toBe(true);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should return true for empty array', () => {
|
|
424
|
+
expect(applyOperator('not_has_tag', [], 'tag')).toBe(true);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// ============================================================================
|
|
430
|
+
// Boolean Operators
|
|
431
|
+
// ============================================================================
|
|
432
|
+
|
|
433
|
+
describe('Boolean Operators', () => {
|
|
434
|
+
describe('is_true', () => {
|
|
435
|
+
it('should match true', () => {
|
|
436
|
+
expect(applyOperator('is_true', true, null)).toBe(true);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should not match false', () => {
|
|
440
|
+
expect(applyOperator('is_true', false, null)).toBe(false);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should not match truthy values', () => {
|
|
444
|
+
expect(applyOperator('is_true', 1, null)).toBe(false);
|
|
445
|
+
expect(applyOperator('is_true', 'true', null)).toBe(false);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should not match null', () => {
|
|
449
|
+
expect(applyOperator('is_true', null, null)).toBe(false);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
describe('is_false', () => {
|
|
454
|
+
it('should match false', () => {
|
|
455
|
+
expect(applyOperator('is_false', false, null)).toBe(true);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('should not match true', () => {
|
|
459
|
+
expect(applyOperator('is_false', true, null)).toBe(false);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('should not match falsy values', () => {
|
|
463
|
+
expect(applyOperator('is_false', 0, null)).toBe(false);
|
|
464
|
+
expect(applyOperator('is_false', '', null)).toBe(false);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should not match null', () => {
|
|
468
|
+
expect(applyOperator('is_false', null, null)).toBe(false);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// ============================================================================
|
|
474
|
+
// Date Handling
|
|
475
|
+
// ============================================================================
|
|
476
|
+
|
|
477
|
+
describe('Date Handling', () => {
|
|
478
|
+
it('should compare Date objects', () => {
|
|
479
|
+
const date1 = new Date('2024-01-15');
|
|
480
|
+
const date2 = new Date('2024-01-01');
|
|
481
|
+
expect(applyOperator('gt', date1, date2)).toBe(true);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should compare date strings', () => {
|
|
485
|
+
expect(applyOperator('gt', '2024-01-15', '2024-01-01')).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('should compare Date with string', () => {
|
|
489
|
+
const date = new Date('2024-01-15');
|
|
490
|
+
expect(applyOperator('eq', date, '2024-01-15')).toBe(true);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('should handle invalid dates', () => {
|
|
494
|
+
expect(applyOperator('gt', 'invalid-date', '2024-01-01')).toBe(false);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// ============================================================================
|
|
499
|
+
// Type Coercion
|
|
500
|
+
// ============================================================================
|
|
501
|
+
|
|
502
|
+
describe('Type Coercion', () => {
|
|
503
|
+
it('should handle numeric string comparisons', () => {
|
|
504
|
+
expect(applyOperator('gt', '10', '5')).toBe(true);
|
|
505
|
+
expect(applyOperator('lt', '5', '10')).toBe(true);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should handle mixed number and string', () => {
|
|
509
|
+
expect(applyOperator('eq', 42, '42')).toBe(true);
|
|
510
|
+
expect(applyOperator('gt', 10, '5')).toBe(true);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('should handle decimal strings', () => {
|
|
514
|
+
expect(applyOperator('gt', '10.5', '10.2')).toBe(true);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('should not coerce non-numeric strings', () => {
|
|
518
|
+
expect(applyOperator('eq', 'hello', 123)).toBe(false);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// ============================================================================
|
|
523
|
+
// Error Handling
|
|
524
|
+
// ============================================================================
|
|
525
|
+
|
|
526
|
+
describe('Error Handling', () => {
|
|
527
|
+
it('should throw for unknown operator', () => {
|
|
528
|
+
expect(() => applyOperator('unknown' as any, 'test', 'test')).toThrow('Unknown operator');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should handle invalid regex gracefully', () => {
|
|
532
|
+
expect(applyOperator('matches', 'test', '[invalid')).toBe(false);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('should handle null/undefined gracefully', () => {
|
|
536
|
+
expect(applyOperator('contains', null, 'test')).toBe(false);
|
|
537
|
+
expect(applyOperator('startswith', undefined, 'test')).toBe(false);
|
|
538
|
+
});
|
|
539
|
+
});
|