@tehw0lf/yaft 0.0.2 → 0.0.4
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/.github/workflows/build.yml +26 -0
- package/CLAUDE.md +138 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/jest.config.js +13 -0
- package/logo.svg +27 -0
- package/package.json +12 -2
- package/src/FeatureToggle.ts +80 -0
- package/src/examples/ApiServiceBooleanProvider.ts +44 -0
- package/src/examples/ApiServiceFeatureProvider.ts +49 -0
- package/src/examples/LocalStorageBooleanProvider.ts +25 -0
- package/src/examples/LocalStorageFeatureProvider.ts +30 -0
- package/src/index.ts +6 -0
- package/src/test/decorator-behavior.spec.ts +359 -0
- package/src/test/error-handling.spec.ts +391 -0
- package/src/test/feature-data.spec.ts +102 -0
- package/src/test/integration.spec.ts +472 -0
- package/src/test/providers.spec.ts +218 -0
- package/src/test/test-boolean.json +1 -0
- package/src/test/test-feature.json +20 -0
- package/src/test/test-setup.ts +2 -0
- package/src/test/yaft.demo.spec.ts +53 -0
- package/tsconfig.json +10 -0
- package/tsconfig.spec.json +10 -0
- package/FeatureToggle.d.ts +0 -19
- package/FeatureToggle.js +0 -60
- package/index.d.ts +0 -1
- package/index.js +0 -6
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { FeatureToggle, FeatureToggleBase, FeatureProvider, Feature } from '../FeatureToggle';
|
|
2
|
+
import { LocalStorageFeatureProvider } from '../examples/LocalStorageFeatureProvider';
|
|
3
|
+
import { ApiServiceFeatureProvider } from '../examples/ApiServiceFeatureProvider';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
|
|
6
|
+
// Mock axios for API provider tests
|
|
7
|
+
jest.mock('axios');
|
|
8
|
+
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
|
9
|
+
|
|
10
|
+
describe('Error Handling and Edge Cases', () => {
|
|
11
|
+
let mockProvider: FeatureProvider<boolean>;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockProvider = {
|
|
15
|
+
data: {},
|
|
16
|
+
getConfig: jest.fn(),
|
|
17
|
+
isEnabled: jest.fn()
|
|
18
|
+
};
|
|
19
|
+
FeatureToggleBase.featureProvider = mockProvider;
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('Provider Initialization Errors', () => {
|
|
24
|
+
it('should handle missing provider gracefully', () => {
|
|
25
|
+
FeatureToggleBase.featureProvider = null as any;
|
|
26
|
+
|
|
27
|
+
expect(() => {
|
|
28
|
+
class TestClass {
|
|
29
|
+
@FeatureToggle('test')
|
|
30
|
+
method() {}
|
|
31
|
+
}
|
|
32
|
+
}).toThrow('FeatureToggleProvider not set');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should handle undefined provider gracefully', () => {
|
|
36
|
+
FeatureToggleBase.featureProvider = undefined as any;
|
|
37
|
+
|
|
38
|
+
expect(() => {
|
|
39
|
+
class TestClass {
|
|
40
|
+
@FeatureToggle('test')
|
|
41
|
+
method() {}
|
|
42
|
+
}
|
|
43
|
+
}).toThrow('FeatureToggleProvider not set');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('LocalStorageFeatureProvider Error Handling', () => {
|
|
48
|
+
it('should handle non-existent configuration files', () => {
|
|
49
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
50
|
+
|
|
51
|
+
const provider = new LocalStorageFeatureProvider('./non-existent-file.json');
|
|
52
|
+
|
|
53
|
+
expect(provider.data).toEqual({});
|
|
54
|
+
expect(provider.isEnabled('anyKey')).toBe(false);
|
|
55
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
56
|
+
'Failed to load configuration from local file:',
|
|
57
|
+
expect.objectContaining({
|
|
58
|
+
code: 'MODULE_NOT_FOUND'
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
consoleSpy.mockRestore();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should handle corrupted JSON files', () => {
|
|
66
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
67
|
+
const originalRequire = require;
|
|
68
|
+
|
|
69
|
+
// Mock require to throw JSON parsing error
|
|
70
|
+
(global as any).require = jest.fn(() => {
|
|
71
|
+
throw new SyntaxError('Unexpected token in JSON');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const provider = new LocalStorageFeatureProvider('./corrupted.json');
|
|
75
|
+
|
|
76
|
+
expect(provider.data).toEqual({});
|
|
77
|
+
expect(provider.isEnabled('anyKey')).toBe(false);
|
|
78
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
79
|
+
|
|
80
|
+
// Restore
|
|
81
|
+
(global as any).require = originalRequire;
|
|
82
|
+
consoleSpy.mockRestore();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should handle features with malformed date strings', () => {
|
|
86
|
+
const provider = new (class extends LocalStorageFeatureProvider {
|
|
87
|
+
constructor() {
|
|
88
|
+
super('');
|
|
89
|
+
this.data = {
|
|
90
|
+
'malformedDates': {
|
|
91
|
+
key: 'malformedDates',
|
|
92
|
+
value: 'true',
|
|
93
|
+
activeAt: 'invalid-date',
|
|
94
|
+
disabledAt: 'also-invalid-date'
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
})();
|
|
99
|
+
|
|
100
|
+
// Date.parse returns NaN for invalid dates, which should be handled
|
|
101
|
+
const result = provider.isEnabled('malformedDates');
|
|
102
|
+
expect(typeof result).toBe('boolean');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should handle features with null/undefined date values', () => {
|
|
106
|
+
const provider = new (class extends LocalStorageFeatureProvider {
|
|
107
|
+
constructor() {
|
|
108
|
+
super('');
|
|
109
|
+
this.data = {
|
|
110
|
+
'nullDates': {
|
|
111
|
+
key: 'nullDates',
|
|
112
|
+
value: 'true',
|
|
113
|
+
activeAt: null as any,
|
|
114
|
+
disabledAt: undefined as any
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
|
|
120
|
+
const result = provider.isEnabled('nullDates');
|
|
121
|
+
expect(typeof result).toBe('boolean');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle features with invalid boolean values', () => {
|
|
125
|
+
const provider = new (class extends LocalStorageFeatureProvider {
|
|
126
|
+
constructor() {
|
|
127
|
+
super('');
|
|
128
|
+
this.data = {
|
|
129
|
+
'invalidBoolean': {
|
|
130
|
+
key: 'invalidBoolean',
|
|
131
|
+
value: 'not-a-boolean',
|
|
132
|
+
activeAt: '',
|
|
133
|
+
disabledAt: ''
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
|
|
139
|
+
expect(() => {
|
|
140
|
+
provider.isEnabled('invalidBoolean');
|
|
141
|
+
}).toThrow(); // JSON.parse should throw for invalid boolean strings
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('ApiServiceFeatureProvider Error Handling', () => {
|
|
146
|
+
it('should handle network failures gracefully', async () => {
|
|
147
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
148
|
+
mockedAxios.get.mockRejectedValue(new Error('Network error'));
|
|
149
|
+
|
|
150
|
+
const provider = new ApiServiceFeatureProvider('https://api.test.com', 'test-uuid');
|
|
151
|
+
|
|
152
|
+
// Allow async operations to complete
|
|
153
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
154
|
+
|
|
155
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
156
|
+
'Failed to fetch feature toggle from API:',
|
|
157
|
+
expect.any(Error)
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
consoleSpy.mockRestore();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should handle API returning invalid data format', async () => {
|
|
164
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
165
|
+
mockedAxios.get
|
|
166
|
+
.mockResolvedValueOnce({ data: { value: 'invalid-hash' } }) // collection hash
|
|
167
|
+
.mockResolvedValueOnce({ data: { value: 'not-an-object' } }); // features
|
|
168
|
+
|
|
169
|
+
const provider = new ApiServiceFeatureProvider('https://api.test.com', 'test-uuid');
|
|
170
|
+
|
|
171
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
172
|
+
|
|
173
|
+
// Should handle gracefully without crashing
|
|
174
|
+
expect(provider.data).toBeDefined();
|
|
175
|
+
|
|
176
|
+
consoleSpy.mockRestore();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should handle HTTP error responses', async () => {
|
|
180
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
181
|
+
mockedAxios.get.mockRejectedValue({
|
|
182
|
+
response: { status: 404, statusText: 'Not Found' }
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const provider = new ApiServiceFeatureProvider('https://api.test.com', 'test-uuid');
|
|
186
|
+
|
|
187
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
188
|
+
|
|
189
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
190
|
+
|
|
191
|
+
consoleSpy.mockRestore();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should handle timeout errors', async () => {
|
|
195
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
196
|
+
mockedAxios.get.mockRejectedValue({ code: 'ECONNABORTED', message: 'timeout' });
|
|
197
|
+
|
|
198
|
+
const provider = new ApiServiceFeatureProvider('https://api.test.com', 'test-uuid');
|
|
199
|
+
|
|
200
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
201
|
+
|
|
202
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
203
|
+
|
|
204
|
+
consoleSpy.mockRestore();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('Decorator Edge Cases', () => {
|
|
209
|
+
beforeEach(() => {
|
|
210
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should handle methods throwing errors in fallbacks', () => {
|
|
214
|
+
function errorFallback() {
|
|
215
|
+
throw new Error('Fallback error');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
class TestClass {
|
|
219
|
+
@FeatureToggle('disabled', errorFallback)
|
|
220
|
+
method() {
|
|
221
|
+
return 'original';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const instance = new TestClass();
|
|
226
|
+
|
|
227
|
+
expect(() => {
|
|
228
|
+
instance.method();
|
|
229
|
+
}).toThrow('Fallback error');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should handle async methods throwing errors', async () => {
|
|
233
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(true);
|
|
234
|
+
|
|
235
|
+
class TestClass {
|
|
236
|
+
@FeatureToggle('enabled')
|
|
237
|
+
async asyncMethod() {
|
|
238
|
+
throw new Error('Async error');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const instance = new TestClass();
|
|
243
|
+
|
|
244
|
+
await expect(instance.asyncMethod()).rejects.toThrow('Async error');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should handle circular reference in fallback class', () => {
|
|
248
|
+
class CircularClass {
|
|
249
|
+
constructor() {
|
|
250
|
+
(this as any).self = this;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@FeatureToggle('disabled', CircularClass)
|
|
255
|
+
class TestClass {
|
|
256
|
+
constructor() {}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
expect(() => {
|
|
260
|
+
new TestClass();
|
|
261
|
+
}).not.toThrow();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should handle fallback class constructor throwing errors', () => {
|
|
265
|
+
class ErrorClass {
|
|
266
|
+
constructor() {
|
|
267
|
+
throw new Error('Constructor error');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@FeatureToggle('disabled', ErrorClass)
|
|
272
|
+
class TestClass {
|
|
273
|
+
constructor() {}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
expect(() => {
|
|
277
|
+
new TestClass();
|
|
278
|
+
}).toThrow('Constructor error');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should handle deeply nested method calls', () => {
|
|
282
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(true);
|
|
283
|
+
|
|
284
|
+
class TestClass {
|
|
285
|
+
@FeatureToggle('enabled')
|
|
286
|
+
level1() {
|
|
287
|
+
return this.level2();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
level2() {
|
|
291
|
+
return this.level3();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
level3() {
|
|
295
|
+
return 'deep result';
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const instance = new TestClass();
|
|
300
|
+
expect(instance.level1()).toBe('deep result');
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('Memory and Performance Edge Cases', () => {
|
|
305
|
+
it('should handle large number of decorated methods', () => {
|
|
306
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(true);
|
|
307
|
+
|
|
308
|
+
class TestClass {
|
|
309
|
+
@FeatureToggle('feature1') method1() { return 1; }
|
|
310
|
+
@FeatureToggle('feature2') method2() { return 2; }
|
|
311
|
+
@FeatureToggle('feature3') method3() { return 3; }
|
|
312
|
+
@FeatureToggle('feature4') method4() { return 4; }
|
|
313
|
+
@FeatureToggle('feature5') method5() { return 5; }
|
|
314
|
+
// ... many more methods would be here in a real test
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const instance = new TestClass();
|
|
318
|
+
|
|
319
|
+
expect(instance.method1()).toBe(1);
|
|
320
|
+
expect(instance.method5()).toBe(5);
|
|
321
|
+
expect(mockProvider.isEnabled).toHaveBeenCalledTimes(5);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should handle rapid instantiation of decorated classes', () => {
|
|
325
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(false);
|
|
326
|
+
|
|
327
|
+
@FeatureToggle('disabled')
|
|
328
|
+
class TestClass {
|
|
329
|
+
value = Math.random();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const instances = Array.from({ length: 100 }, () => new TestClass());
|
|
333
|
+
|
|
334
|
+
expect(instances).toHaveLength(100);
|
|
335
|
+
expect(mockProvider.isEnabled).toHaveBeenCalledTimes(1); // Called once during decoration
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should handle very long feature keys', () => {
|
|
339
|
+
const longKey = 'a'.repeat(10000);
|
|
340
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(true);
|
|
341
|
+
|
|
342
|
+
class TestClass {
|
|
343
|
+
@FeatureToggle(longKey)
|
|
344
|
+
method() {
|
|
345
|
+
return 'result';
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const instance = new TestClass();
|
|
350
|
+
expect(instance.method()).toBe('result');
|
|
351
|
+
expect(mockProvider.isEnabled).toHaveBeenCalledWith(longKey);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe('Type Safety Edge Cases', () => {
|
|
356
|
+
it('should handle mixed data types in providers', () => {
|
|
357
|
+
const mixedProvider = {
|
|
358
|
+
data: {
|
|
359
|
+
'stringFeature': 'true',
|
|
360
|
+
'booleanFeature': true,
|
|
361
|
+
'numberFeature': 1,
|
|
362
|
+
'objectFeature': { enabled: true },
|
|
363
|
+
'nullFeature': null,
|
|
364
|
+
'undefinedFeature': undefined
|
|
365
|
+
},
|
|
366
|
+
getConfig: jest.fn(),
|
|
367
|
+
isEnabled: (key: string) => {
|
|
368
|
+
const value = mixedProvider.data[key];
|
|
369
|
+
if (value === null || value === undefined) return false;
|
|
370
|
+
return Boolean(value);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
FeatureToggleBase.featureProvider = mixedProvider;
|
|
375
|
+
|
|
376
|
+
class TestClass {
|
|
377
|
+
@FeatureToggle('stringFeature') stringMethod() { return 'string'; }
|
|
378
|
+
@FeatureToggle('booleanFeature') booleanMethod() { return 'boolean'; }
|
|
379
|
+
@FeatureToggle('numberFeature') numberMethod() { return 'number'; }
|
|
380
|
+
@FeatureToggle('nullFeature') nullMethod() { return 'null'; }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const instance = new TestClass();
|
|
384
|
+
|
|
385
|
+
expect(instance.stringMethod()).toBe('string');
|
|
386
|
+
expect(instance.booleanMethod()).toBe('boolean');
|
|
387
|
+
expect(instance.numberMethod()).toBe('number');
|
|
388
|
+
expect(instance.nullMethod()).toBeUndefined(); // null should be falsy
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Feature } from '../FeatureToggle';
|
|
2
|
+
|
|
3
|
+
describe('Feature Data Type', () => {
|
|
4
|
+
describe('Feature structure validation', () => {
|
|
5
|
+
it('should accept valid Feature objects', () => {
|
|
6
|
+
const validFeature: Feature = {
|
|
7
|
+
key: 'testFeature',
|
|
8
|
+
value: 'true',
|
|
9
|
+
activeAt: '2023-01-01T00:00:00Z',
|
|
10
|
+
disabledAt: '2023-12-31T23:59:59Z'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(validFeature.key).toBe('testFeature');
|
|
14
|
+
expect(validFeature.value).toBe('true');
|
|
15
|
+
expect(validFeature.activeAt).toBe('2023-01-01T00:00:00Z');
|
|
16
|
+
expect(validFeature.disabledAt).toBe('2023-12-31T23:59:59Z');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should accept Feature with empty date strings', () => {
|
|
20
|
+
const featureWithEmptyDates: Feature = {
|
|
21
|
+
key: 'testFeature',
|
|
22
|
+
value: 'false',
|
|
23
|
+
activeAt: '',
|
|
24
|
+
disabledAt: ''
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
expect(featureWithEmptyDates.activeAt).toBe('');
|
|
28
|
+
expect(featureWithEmptyDates.disabledAt).toBe('');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should accept boolean-like string values', () => {
|
|
32
|
+
const trueBooleanFeature: Feature = {
|
|
33
|
+
key: 'trueFeature',
|
|
34
|
+
value: 'true',
|
|
35
|
+
activeAt: '',
|
|
36
|
+
disabledAt: ''
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const falseBooleanFeature: Feature = {
|
|
40
|
+
key: 'falseFeature',
|
|
41
|
+
value: 'false',
|
|
42
|
+
activeAt: '',
|
|
43
|
+
disabledAt: ''
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
expect(JSON.parse(trueBooleanFeature.value)).toBe(true);
|
|
47
|
+
expect(JSON.parse(falseBooleanFeature.value)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should handle various date formats', () => {
|
|
51
|
+
const isoDateFeature: Feature = {
|
|
52
|
+
key: 'isoFeature',
|
|
53
|
+
value: 'true',
|
|
54
|
+
activeAt: '2023-01-01T00:00:00.000Z',
|
|
55
|
+
disabledAt: '2023-12-31T23:59:59.999Z'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const simpleDateFeature: Feature = {
|
|
59
|
+
key: 'simpleFeature',
|
|
60
|
+
value: 'true',
|
|
61
|
+
activeAt: '2023-01-01',
|
|
62
|
+
disabledAt: '2023-12-31'
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
expect(Date.parse(isoDateFeature.activeAt)).not.toBeNaN();
|
|
66
|
+
expect(Date.parse(isoDateFeature.disabledAt)).not.toBeNaN();
|
|
67
|
+
expect(Date.parse(simpleDateFeature.activeAt)).not.toBeNaN();
|
|
68
|
+
expect(Date.parse(simpleDateFeature.disabledAt)).not.toBeNaN();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('Feature collection structures', () => {
|
|
73
|
+
it('should handle Record<string, Feature> collections', () => {
|
|
74
|
+
const featureCollection: Record<string, Feature> = {
|
|
75
|
+
'feature1': {
|
|
76
|
+
key: 'feature1',
|
|
77
|
+
value: 'true',
|
|
78
|
+
activeAt: '',
|
|
79
|
+
disabledAt: ''
|
|
80
|
+
},
|
|
81
|
+
'feature2': {
|
|
82
|
+
key: 'feature2',
|
|
83
|
+
value: 'false',
|
|
84
|
+
activeAt: '2023-01-01',
|
|
85
|
+
disabledAt: '2023-12-31'
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
expect(Object.keys(featureCollection)).toHaveLength(2);
|
|
90
|
+
expect(featureCollection['feature1']).toBeDefined();
|
|
91
|
+
expect(featureCollection['feature2']).toBeDefined();
|
|
92
|
+
expect(featureCollection['nonexistent']).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle empty feature collections', () => {
|
|
96
|
+
const emptyCollection: Record<string, Feature> = {};
|
|
97
|
+
|
|
98
|
+
expect(Object.keys(emptyCollection)).toHaveLength(0);
|
|
99
|
+
expect(emptyCollection['anyKey']).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|