@plyaz/types 1.3.2 → 1.3.3
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/dist/api/index.d.ts +1 -0
- package/dist/api/types.d.ts +84 -0
- package/dist/auth/enums.d.ts +32 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/schemas.d.ts +27 -0
- package/dist/auth/types.d.ts +34 -0
- package/dist/common/index.d.ts +2 -0
- package/dist/common/types.d.ts +22 -0
- package/dist/entities/index.d.ts +1 -0
- package/dist/errors/enums.d.ts +33 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/types.d.ts +79 -0
- package/dist/events/enums.d.ts +25 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/payload.d.ts +6 -0
- package/dist/events/types.d.ts +136 -0
- package/dist/features/cache/index.d.ts +1 -0
- package/dist/features/cache/types.d.ts +142 -0
- package/dist/features/feature-flag/index.d.ts +1 -0
- package/dist/features/feature-flag/types.d.ts +491 -0
- package/dist/features/index.d.ts +2 -0
- package/dist/index.d.ts +12 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/testing/common/assertions/index.d.ts +1 -0
- package/dist/testing/common/assertions/types.d.ts +137 -0
- package/dist/testing/common/factories/index.d.ts +1 -0
- package/dist/testing/common/factories/types.d.ts +701 -0
- package/dist/testing/common/index.d.ts +6 -0
- package/dist/testing/common/mocks/index.d.ts +1 -0
- package/dist/testing/common/mocks/types.d.ts +1662 -0
- package/dist/testing/common/patterns/index.d.ts +1 -0
- package/dist/testing/common/patterns/types.d.ts +397 -0
- package/dist/testing/common/utils/index.d.ts +1 -0
- package/dist/testing/common/utils/types.d.ts +1970 -0
- package/dist/testing/common/wrappers/index.d.ts +1 -0
- package/dist/testing/common/wrappers/types.d.ts +373 -0
- package/dist/testing/features/cache/index.d.ts +1 -0
- package/dist/testing/features/cache/types.d.ts +43 -0
- package/dist/testing/features/feature-flags/index.d.ts +1 -0
- package/dist/testing/features/feature-flags/types.d.ts +1133 -0
- package/dist/testing/features/index.d.ts +2 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/translations/index.d.ts +1 -0
- package/dist/translations/types.d.ts +390 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/web3/enums.d.ts +17 -0
- package/dist/web3/index.d.ts +2 -0
- package/dist/web3/types.d.ts +63 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Feature Flag Testing Types
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive type definitions for testing feature flag functionality across
|
|
5
|
+
* different providers, evaluation strategies, and contexts. Provides type safety
|
|
6
|
+
* for feature flag test scenarios, mocking, and validation.
|
|
7
|
+
*
|
|
8
|
+
* This module includes types for:
|
|
9
|
+
* - Feature flag builders and factories
|
|
10
|
+
* - Mock feature flag providers
|
|
11
|
+
* - Feature flag test scenarios and assertions
|
|
12
|
+
* - Context and evaluation testing
|
|
13
|
+
* - Integration with React components and hooks
|
|
14
|
+
* - Provider switching and configuration testing
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import type {
|
|
19
|
+
* FeatureFlagBuilder,
|
|
20
|
+
* MockFeatureFlagProvider,
|
|
21
|
+
* FeatureFlagTestScenario
|
|
22
|
+
* } from './types';
|
|
23
|
+
*
|
|
24
|
+
* // Build test feature flags
|
|
25
|
+
* const flag = featureFlagBuilder
|
|
26
|
+
* .withKey('new-checkout')
|
|
27
|
+
* .withValue(true)
|
|
28
|
+
* .withContext({ userId: '123' })
|
|
29
|
+
* .build();
|
|
30
|
+
*
|
|
31
|
+
* // Mock provider for testing
|
|
32
|
+
* const mockProvider: MockFeatureFlagProvider = createMockProvider();
|
|
33
|
+
* mockProvider.get.mockResolvedValue(true);
|
|
34
|
+
*
|
|
35
|
+
* // Test scenarios
|
|
36
|
+
* const scenario: FeatureFlagTestScenario = {
|
|
37
|
+
* name: 'Checkout flag enabled',
|
|
38
|
+
* flags: { 'new-checkout': true },
|
|
39
|
+
* expectedBehavior: () => expect(checkoutButton).toBeVisible()
|
|
40
|
+
* };
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @module FeatureFlagTestingTypes
|
|
44
|
+
* @since 1.0.0
|
|
45
|
+
*/
|
|
46
|
+
import type * as Vitest from 'vitest';
|
|
47
|
+
import type * as React from 'react';
|
|
48
|
+
import type { FeatureFlag, FeatureFlagCondition, FeatureFlagConfig, FeatureFlagContext, FeatureFlagEvaluation, FeatureFlagRule, FeatureFlagValue, FeatureFlagProvider as IFeatureFlagProvider, FeatureFlagContextValue } from '../../../features';
|
|
49
|
+
import type { MockLogger, RenderFunction, RenderHookFunction } from '../../common';
|
|
50
|
+
/**
|
|
51
|
+
* Builder interface for creating feature flag test objects with fluent API
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const flag = featureFlagBuilder
|
|
56
|
+
* .withKey('new-checkout')
|
|
57
|
+
* .withValue(true)
|
|
58
|
+
* .withEnabled(true)
|
|
59
|
+
* .withRolloutPercentage(50)
|
|
60
|
+
* .withCondition({ field: 'userId', operator: 'in', value: ['user1', 'user2'] })
|
|
61
|
+
* .build();
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export interface FeatureFlagBuilder<FeatureFlagKey extends string> {
|
|
65
|
+
/** Set the feature flag key */
|
|
66
|
+
withKey: (key: FeatureFlagKey) => FeatureFlagBuilder<FeatureFlagKey>;
|
|
67
|
+
/** Set the feature flag value */
|
|
68
|
+
withValue: (value: FeatureFlagValue) => FeatureFlagBuilder<FeatureFlagKey>;
|
|
69
|
+
/** Set whether the flag is enabled */
|
|
70
|
+
withEnabled: (enabled: boolean) => FeatureFlagBuilder<FeatureFlagKey>;
|
|
71
|
+
/** Set the flag description */
|
|
72
|
+
withDescription: (description: string) => FeatureFlagBuilder<FeatureFlagKey>;
|
|
73
|
+
/** Set the rollout percentage (0-100) */
|
|
74
|
+
withRolloutPercentage: (percentage: number) => FeatureFlagBuilder<FeatureFlagKey>;
|
|
75
|
+
/** Add a single condition */
|
|
76
|
+
withCondition: (condition: FeatureFlagCondition) => FeatureFlagBuilder<FeatureFlagKey>;
|
|
77
|
+
/** Set multiple conditions */
|
|
78
|
+
withConditions: (conditions: FeatureFlagCondition[]) => FeatureFlagBuilder<FeatureFlagKey>;
|
|
79
|
+
/** Set the creation date */
|
|
80
|
+
withCreatedAt: (date: Date) => FeatureFlagBuilder<FeatureFlagKey>;
|
|
81
|
+
/** Set the last update date */
|
|
82
|
+
withUpdatedAt: (date: Date) => FeatureFlagBuilder<FeatureFlagKey>;
|
|
83
|
+
/** Build the final feature flag object */
|
|
84
|
+
build: () => FeatureFlag<FeatureFlagKey>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Mock objects for feature flag module testing
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* const mocks: SetupFeatureFlagModuleMocks = {
|
|
92
|
+
* mockLogger: createMockLogger(),
|
|
93
|
+
* HttpException: vi.fn(),
|
|
94
|
+
* HttpStatus: { OK: 200, NOT_FOUND: 404, INTERNAL_SERVER_ERROR: 500 }
|
|
95
|
+
* };
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export interface SetupFeatureFlagModuleMocks {
|
|
99
|
+
/** Mock logger instance */
|
|
100
|
+
mockLogger: MockLogger;
|
|
101
|
+
/** Mock HTTP exception constructor */
|
|
102
|
+
HttpException: Vitest.Mock;
|
|
103
|
+
/** HTTP status code constants */
|
|
104
|
+
HttpStatus: Record<string, number>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Builder interface for creating feature flag context objects
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* const context = contextBuilder
|
|
112
|
+
* .withUserId('user-123')
|
|
113
|
+
* .withEnvironment('production')
|
|
114
|
+
* .withCountry('US')
|
|
115
|
+
* .withCustom({ tier: 'premium', beta: true })
|
|
116
|
+
* .build();
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export interface FeatureFlagContextBuilder {
|
|
120
|
+
/** Set the user identifier */
|
|
121
|
+
withUserId: (userId: string) => FeatureFlagContextBuilder;
|
|
122
|
+
/** Set the group identifier */
|
|
123
|
+
withGroupId: (groupId: string) => FeatureFlagContextBuilder;
|
|
124
|
+
/** Set the environment (dev, staging, prod) */
|
|
125
|
+
withEnvironment: (environment: string) => FeatureFlagContextBuilder;
|
|
126
|
+
/** Set the user's country */
|
|
127
|
+
withCountry: (country: string) => FeatureFlagContextBuilder;
|
|
128
|
+
/** Set the user's language */
|
|
129
|
+
withLanguage: (language: string) => FeatureFlagContextBuilder;
|
|
130
|
+
/** Set the platform (web, mobile, api) */
|
|
131
|
+
withPlatform: (platform: string) => FeatureFlagContextBuilder;
|
|
132
|
+
/** Set the application version */
|
|
133
|
+
withVersion: (version: string) => FeatureFlagContextBuilder;
|
|
134
|
+
/** Set custom context properties */
|
|
135
|
+
withCustom: (custom: Record<string, unknown>) => FeatureFlagContextBuilder;
|
|
136
|
+
/** Build the final context object */
|
|
137
|
+
build: () => FeatureFlagContext;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Builder interface for creating feature flag condition objects
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const condition = conditionBuilder
|
|
145
|
+
* .withField('userId')
|
|
146
|
+
* .withOperator('in')
|
|
147
|
+
* .withValue(['user1', 'user2', 'user3'])
|
|
148
|
+
* .build();
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export interface FeatureFlagConditionBuilder {
|
|
152
|
+
/** Set the field to evaluate */
|
|
153
|
+
withField: (field: FeatureFlagCondition['field']) => FeatureFlagConditionBuilder;
|
|
154
|
+
/** Set the comparison operator */
|
|
155
|
+
withOperator: (operator: FeatureFlagCondition['operator']) => FeatureFlagConditionBuilder;
|
|
156
|
+
/** Set the comparison value(s) */
|
|
157
|
+
withValue: (value: string | number | string[] | number[]) => FeatureFlagConditionBuilder;
|
|
158
|
+
/** Build the final condition object */
|
|
159
|
+
build: () => FeatureFlagCondition;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Factory interface for creating various types of feature flag test objects
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```typescript
|
|
166
|
+
* const factory: FeatureFlagFactory = createFeatureFlagFactory();
|
|
167
|
+
*
|
|
168
|
+
* // Create basic flag
|
|
169
|
+
* const flag = factory.create({ key: 'my-flag', enabled: true });
|
|
170
|
+
*
|
|
171
|
+
* // Create rollout flag
|
|
172
|
+
* const rolloutFlag = factory.createWithRollout(25, { key: 'gradual-rollout' });
|
|
173
|
+
*
|
|
174
|
+
* // Create typed flags
|
|
175
|
+
* const boolFlag = factory.createBoolean('feature-enabled', true);
|
|
176
|
+
* const stringFlag = factory.createString('theme', 'dark');
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export interface FeatureFlagFactory<FeatureFlagKey extends string> {
|
|
180
|
+
/** Create a basic feature flag */
|
|
181
|
+
create: (overrides?: Partial<FeatureFlag<FeatureFlagKey>>) => FeatureFlag<FeatureFlagKey>;
|
|
182
|
+
/** Create multiple feature flags */
|
|
183
|
+
createList: (count: number, overrides?: Partial<FeatureFlag<FeatureFlagKey>>) => FeatureFlag<FeatureFlagKey>[];
|
|
184
|
+
/** Create an enabled feature flag */
|
|
185
|
+
createEnabled: (overrides?: Partial<FeatureFlag<FeatureFlagKey>>) => FeatureFlag<FeatureFlagKey>;
|
|
186
|
+
/** Create a disabled feature flag */
|
|
187
|
+
createDisabled: (overrides?: Partial<FeatureFlag<FeatureFlagKey>>) => FeatureFlag<FeatureFlagKey>;
|
|
188
|
+
/** Create a feature flag with rollout percentage */
|
|
189
|
+
createWithRollout: (percentage: number, overrides?: Partial<FeatureFlag<FeatureFlagKey>>) => FeatureFlag<FeatureFlagKey>;
|
|
190
|
+
/** Create a feature flag with conditions */
|
|
191
|
+
createWithConditions: (conditions: FeatureFlagCondition[], overrides?: Partial<FeatureFlag<FeatureFlagKey>>) => FeatureFlag<FeatureFlagKey>;
|
|
192
|
+
/** Create a boolean feature flag */
|
|
193
|
+
createBoolean: (key: string, value: boolean, overrides?: Partial<FeatureFlag<FeatureFlagKey>>) => FeatureFlag<FeatureFlagKey>;
|
|
194
|
+
/** Create a string feature flag */
|
|
195
|
+
createString: (key: string, value: string, overrides?: Partial<FeatureFlag<FeatureFlagKey>>) => FeatureFlag<FeatureFlagKey>;
|
|
196
|
+
/** Create a number feature flag */
|
|
197
|
+
createNumber: (key: string, value: number, overrides?: Partial<FeatureFlag<FeatureFlagKey>>) => FeatureFlag<FeatureFlagKey>;
|
|
198
|
+
/** Create an object feature flag */
|
|
199
|
+
createObject: (key: string, value: Record<string, unknown>, overrides?: Partial<FeatureFlag<FeatureFlagKey>>) => FeatureFlag<FeatureFlagKey>;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Pre-defined feature flag scenario generators for common testing patterns
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const scenarios: FeatureFlagScenarios = createFeatureFlagScenarios();
|
|
207
|
+
*
|
|
208
|
+
* // Get basic enabled/disabled flags
|
|
209
|
+
* const basicFlags = scenarios.basic();
|
|
210
|
+
*
|
|
211
|
+
* // Get flags with rollout percentages
|
|
212
|
+
* const rolloutFlags = scenarios.rollout();
|
|
213
|
+
*
|
|
214
|
+
* // Get flags with complex conditions
|
|
215
|
+
* const conditionalFlags = scenarios.conditional();
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
export interface FeatureFlagScenarios<FeatureFlagKey extends string> {
|
|
219
|
+
/** Generate basic enabled/disabled flags */
|
|
220
|
+
basic: () => FeatureFlag<FeatureFlagKey>[];
|
|
221
|
+
/** Generate flags with rollout percentages */
|
|
222
|
+
rollout: () => FeatureFlag<FeatureFlagKey>[];
|
|
223
|
+
/** Generate flags with conditions */
|
|
224
|
+
conditional: () => FeatureFlag<FeatureFlagKey>[];
|
|
225
|
+
/** Generate flags with multiple variants */
|
|
226
|
+
multiVariant: () => FeatureFlag<FeatureFlagKey>[];
|
|
227
|
+
/** Generate flags targeting specific users */
|
|
228
|
+
userTargeting: () => FeatureFlag<FeatureFlagKey>[];
|
|
229
|
+
/** Generate flags targeting user groups */
|
|
230
|
+
groupTargeting: () => FeatureFlag<FeatureFlagKey>[];
|
|
231
|
+
/** Generate flags targeting environments */
|
|
232
|
+
environmentTargeting: () => FeatureFlag<FeatureFlagKey>[];
|
|
233
|
+
/** Generate mixed scenario flags */
|
|
234
|
+
mixed: () => FeatureFlag<FeatureFlagKey>[];
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Mock feature flag provider interface for testing provider interactions
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```typescript
|
|
241
|
+
* const mockProvider: MockFeatureFlagProvider = {
|
|
242
|
+
* getFlag: vi.fn().mockResolvedValue(mockFlag),
|
|
243
|
+
* getAllFlags: vi.fn().mockResolvedValue([mockFlag1, mockFlag2]),
|
|
244
|
+
* evaluateFlag: vi.fn().mockResolvedValue({ value: true, reason: 'enabled' }),
|
|
245
|
+
* isEnabled: vi.fn().mockResolvedValue(true),
|
|
246
|
+
* getValue: vi.fn().mockResolvedValue('default-value'),
|
|
247
|
+
* // ... other methods
|
|
248
|
+
* };
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
export interface MockFeatureFlagProvider {
|
|
252
|
+
/** Get a specific feature flag */
|
|
253
|
+
getFlag: Vitest.Mock;
|
|
254
|
+
/** Get all feature flags */
|
|
255
|
+
getAllFlags: Vitest.Mock;
|
|
256
|
+
/** Evaluate a feature flag */
|
|
257
|
+
evaluateFlag: Vitest.Mock;
|
|
258
|
+
/** Check if a flag is enabled */
|
|
259
|
+
isEnabled: Vitest.Mock;
|
|
260
|
+
/** Get a flag's value */
|
|
261
|
+
getValue: Vitest.Mock;
|
|
262
|
+
/** Update a feature flag */
|
|
263
|
+
updateFlag: Vitest.Mock;
|
|
264
|
+
/** Delete a feature flag */
|
|
265
|
+
deleteFlag: Vitest.Mock;
|
|
266
|
+
/** Refresh flag data */
|
|
267
|
+
refresh: Vitest.Mock;
|
|
268
|
+
/** Subscribe to flag changes */
|
|
269
|
+
subscribe: Vitest.Mock;
|
|
270
|
+
/** Unsubscribe from flag changes */
|
|
271
|
+
unsubscribe: Vitest.Mock;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Mock feature flag repository interface for database operations testing
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```typescript
|
|
278
|
+
* const mockRepository: MockFeatureFlagRepository = {
|
|
279
|
+
* find: vi.fn().mockResolvedValue(mockFlag),
|
|
280
|
+
* findAll: vi.fn().mockResolvedValue([mockFlag1, mockFlag2]),
|
|
281
|
+
* create: vi.fn().mockResolvedValue(newMockFlag),
|
|
282
|
+
* update: vi.fn().mockResolvedValue(updatedMockFlag),
|
|
283
|
+
* delete: vi.fn().mockResolvedValue(true),
|
|
284
|
+
* save: vi.fn().mockResolvedValue(savedMockFlag),
|
|
285
|
+
* exists: vi.fn().mockResolvedValue(true),
|
|
286
|
+
* count: vi.fn().mockResolvedValue(5)
|
|
287
|
+
* };
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
export interface MockFeatureFlagRepository {
|
|
291
|
+
/** Find a single flag by criteria */
|
|
292
|
+
find: Vitest.Mock;
|
|
293
|
+
/** Find all flags */
|
|
294
|
+
findAll: Vitest.Mock;
|
|
295
|
+
/** Create a new flag */
|
|
296
|
+
create: Vitest.Mock;
|
|
297
|
+
/** Update an existing flag */
|
|
298
|
+
update: Vitest.Mock;
|
|
299
|
+
/** Delete a flag */
|
|
300
|
+
delete: Vitest.Mock;
|
|
301
|
+
/** Save a flag */
|
|
302
|
+
save: Vitest.Mock;
|
|
303
|
+
/** Check if a flag exists */
|
|
304
|
+
exists: Vitest.Mock;
|
|
305
|
+
/** Count total flags */
|
|
306
|
+
count: Vitest.Mock;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Mock feature flag service interface for business logic testing
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```typescript
|
|
313
|
+
* const mockService: MockFeatureFlagService = {
|
|
314
|
+
* evaluate: vi.fn().mockResolvedValue({ value: true, reason: 'enabled' }),
|
|
315
|
+
* evaluateAll: vi.fn().mockResolvedValue(evaluationResults),
|
|
316
|
+
* isEnabled: vi.fn().mockResolvedValue(true),
|
|
317
|
+
* getValue: vi.fn().mockResolvedValue('variant-a'),
|
|
318
|
+
* getVariation: vi.fn().mockResolvedValue('control'),
|
|
319
|
+
* track: vi.fn().mockResolvedValue(undefined),
|
|
320
|
+
* refresh: vi.fn().mockResolvedValue(undefined),
|
|
321
|
+
* invalidateCache: vi.fn().mockResolvedValue(undefined)
|
|
322
|
+
* };
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
export interface MockFeatureFlagService {
|
|
326
|
+
/** Evaluate a single flag */
|
|
327
|
+
evaluate: Vitest.Mock;
|
|
328
|
+
/** Evaluate all flags */
|
|
329
|
+
evaluateAll: Vitest.Mock;
|
|
330
|
+
/** Check if flag is enabled */
|
|
331
|
+
isEnabled: Vitest.Mock;
|
|
332
|
+
/** Get flag value */
|
|
333
|
+
getValue: Vitest.Mock;
|
|
334
|
+
/** Get flag variation */
|
|
335
|
+
getVariation: Vitest.Mock;
|
|
336
|
+
/** Track flag usage */
|
|
337
|
+
track: Vitest.Mock;
|
|
338
|
+
/** Refresh flag data */
|
|
339
|
+
refresh: Vitest.Mock;
|
|
340
|
+
/** Clear cache */
|
|
341
|
+
invalidateCache: Vitest.Mock;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Mock feature flag evaluation engine interface for testing flag logic
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```typescript
|
|
348
|
+
* const mockEngine: MockFeatureFlagEngine = {
|
|
349
|
+
* evaluate: vi.fn().mockReturnValue({ value: true, reason: 'match' }),
|
|
350
|
+
* evaluateConditions: vi.fn().mockReturnValue(true),
|
|
351
|
+
* checkRollout: vi.fn().mockReturnValue(true),
|
|
352
|
+
* calculateVariation: vi.fn().mockReturnValue('variant-b'),
|
|
353
|
+
* getDefaultValue: vi.fn().mockReturnValue(false)
|
|
354
|
+
* };
|
|
355
|
+
* ```
|
|
356
|
+
*/
|
|
357
|
+
export interface MockFeatureFlagEngine {
|
|
358
|
+
/** Evaluate flag with context */
|
|
359
|
+
evaluate: Vitest.Mock;
|
|
360
|
+
/** Evaluate conditions */
|
|
361
|
+
evaluateConditions: Vitest.Mock;
|
|
362
|
+
/** Check rollout eligibility */
|
|
363
|
+
checkRollout: Vitest.Mock;
|
|
364
|
+
/** Calculate variation */
|
|
365
|
+
calculateVariation: Vitest.Mock;
|
|
366
|
+
/** Get default value */
|
|
367
|
+
getDefaultValue: Vitest.Mock;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Mock feature flag cache interface for testing caching behavior
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```typescript
|
|
374
|
+
* const mockCache: MockFeatureFlagCache = {
|
|
375
|
+
* get: vi.fn().mockResolvedValue(cachedFlag),
|
|
376
|
+
* set: vi.fn().mockResolvedValue(undefined),
|
|
377
|
+
* delete: vi.fn().mockResolvedValue(true),
|
|
378
|
+
* clear: vi.fn().mockResolvedValue(undefined),
|
|
379
|
+
* has: vi.fn().mockResolvedValue(true),
|
|
380
|
+
* keys: vi.fn().mockResolvedValue(['flag1', 'flag2']),
|
|
381
|
+
* values: vi.fn().mockResolvedValue([flag1, flag2]),
|
|
382
|
+
* size: vi.fn().mockResolvedValue(10)
|
|
383
|
+
* };
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
export interface MockFeatureFlagCache {
|
|
387
|
+
/** Get cached value */
|
|
388
|
+
get: Vitest.Mock;
|
|
389
|
+
/** Set cache value */
|
|
390
|
+
set: Vitest.Mock;
|
|
391
|
+
/** Delete cached value */
|
|
392
|
+
delete: Vitest.Mock;
|
|
393
|
+
/** Clear all cache */
|
|
394
|
+
clear: Vitest.Mock;
|
|
395
|
+
/** Check if key exists */
|
|
396
|
+
has: Vitest.Mock;
|
|
397
|
+
/** Get all cache keys */
|
|
398
|
+
keys: Vitest.Mock;
|
|
399
|
+
/** Get all cache values */
|
|
400
|
+
values: Vitest.Mock;
|
|
401
|
+
/** Get cache size */
|
|
402
|
+
size: Vitest.Mock;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Test context for feature flag testing with React components
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```typescript
|
|
409
|
+
* const testContext: FeatureFlagTestContext = await setupFeatureFlagTest<FeatureFlagKey>({
|
|
410
|
+
* flags: [testFlag1, testFlag2],
|
|
411
|
+
* context: { userId: 'test-user' }
|
|
412
|
+
* });
|
|
413
|
+
*
|
|
414
|
+
* // Use in component tests
|
|
415
|
+
* const { getByText } = testContext.render(
|
|
416
|
+
* <testContext.wrapper>
|
|
417
|
+
* <MyComponent />
|
|
418
|
+
* </testContext.wrapper>
|
|
419
|
+
* );
|
|
420
|
+
*
|
|
421
|
+
* // Clean up after test
|
|
422
|
+
* await testContext.cleanup();
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
export interface FeatureFlagTestContext<FeatureFlagKey extends string> {
|
|
426
|
+
/** Feature flag provider instance */
|
|
427
|
+
provider: IFeatureFlagProvider<FeatureFlagKey>;
|
|
428
|
+
/** Cleanup function for test teardown */
|
|
429
|
+
cleanup: () => Promise<void>;
|
|
430
|
+
/** React wrapper component with provider */
|
|
431
|
+
wrapper: (props: {
|
|
432
|
+
children: React.ReactNode;
|
|
433
|
+
}) => React.ReactNode;
|
|
434
|
+
/** Enhanced render function */
|
|
435
|
+
render: RenderFunction;
|
|
436
|
+
/** Enhanced renderHook function */
|
|
437
|
+
renderHook: RenderHookFunction;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Options for configuring feature flag tests
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```typescript
|
|
444
|
+
* const options: FeatureFlagTestOptions = {
|
|
445
|
+
* flags: [enabledFlag, disabledFlag],
|
|
446
|
+
* context: { userId: 'test-user', environment: 'test' },
|
|
447
|
+
* provider: 'memory',
|
|
448
|
+
* cache: false,
|
|
449
|
+
* timeout: 5000
|
|
450
|
+
* };
|
|
451
|
+
*
|
|
452
|
+
* const testContext = await setupFeatureFlagTest<FeatureFlagKey>(options);
|
|
453
|
+
* ```
|
|
454
|
+
*/
|
|
455
|
+
export interface FeatureFlagTestOptions<FeatureFlagKey extends string> {
|
|
456
|
+
/** Initial flags to load */
|
|
457
|
+
flags?: FeatureFlag<FeatureFlagKey>[];
|
|
458
|
+
/** Default evaluation context */
|
|
459
|
+
context?: FeatureFlagContext;
|
|
460
|
+
/** Provider type to use */
|
|
461
|
+
provider?: 'memory' | 'file' | 'database' | 'api' | 'redis';
|
|
462
|
+
/** Enable caching */
|
|
463
|
+
cache?: boolean;
|
|
464
|
+
/** Enable auto-refresh */
|
|
465
|
+
refresh?: boolean;
|
|
466
|
+
/** Request timeout in milliseconds */
|
|
467
|
+
timeout?: number;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Single feature flag test scenario definition
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* ```typescript
|
|
474
|
+
* const scenario: FeatureFlagScenario = {
|
|
475
|
+
* name: 'Premium user sees new feature',
|
|
476
|
+
* description: 'Premium users should see the new checkout flow',
|
|
477
|
+
* flags: [newCheckoutFlag],
|
|
478
|
+
* context: { userId: 'premium-user-1', tier: 'premium' },
|
|
479
|
+
* expectedResults: {
|
|
480
|
+
* 'new-checkout': true,
|
|
481
|
+
* 'old-checkout': false
|
|
482
|
+
* },
|
|
483
|
+
* setup: async () => {
|
|
484
|
+
* await seedTestData();
|
|
485
|
+
* },
|
|
486
|
+
* teardown: async () => {
|
|
487
|
+
* await cleanupTestData();
|
|
488
|
+
* }
|
|
489
|
+
* };
|
|
490
|
+
* ```
|
|
491
|
+
*/
|
|
492
|
+
export interface FeatureFlagScenario<FeatureFlagKey extends string> {
|
|
493
|
+
/** Scenario name */
|
|
494
|
+
name: string;
|
|
495
|
+
/** Optional scenario description */
|
|
496
|
+
description?: string;
|
|
497
|
+
/** Feature flags for this scenario */
|
|
498
|
+
flags: FeatureFlag<FeatureFlagKey>[];
|
|
499
|
+
/** Evaluation context */
|
|
500
|
+
context?: FeatureFlagContext;
|
|
501
|
+
/** Expected flag evaluation results */
|
|
502
|
+
expectedResults: Record<string, FeatureFlagValue>;
|
|
503
|
+
/** Setup function run before scenario */
|
|
504
|
+
setup?: () => void | Promise<void>;
|
|
505
|
+
/** Teardown function run after scenario */
|
|
506
|
+
teardown?: () => void | Promise<void>;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Test suite for managing and running multiple feature flag scenarios
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* ```typescript
|
|
513
|
+
* const testSuite: FeatureFlagTestSuite = createFeatureFlagTestSuite();
|
|
514
|
+
*
|
|
515
|
+
* // Add scenarios
|
|
516
|
+
* testSuite.addScenario(premiumUserScenario);
|
|
517
|
+
* testSuite.addScenario(freeUserScenario);
|
|
518
|
+
*
|
|
519
|
+
* // Run specific scenario
|
|
520
|
+
* await testSuite.run('Premium user sees new feature');
|
|
521
|
+
*
|
|
522
|
+
* // Run all scenarios
|
|
523
|
+
* await testSuite.runAll();
|
|
524
|
+
*
|
|
525
|
+
* // Clean up
|
|
526
|
+
* testSuite.clear();
|
|
527
|
+
* ```
|
|
528
|
+
*/
|
|
529
|
+
export interface FeatureFlagTestSuite<FeatureFlagKey extends string> {
|
|
530
|
+
/** All scenarios in the suite */
|
|
531
|
+
scenarios: FeatureFlagScenario<FeatureFlagKey>[];
|
|
532
|
+
/** Run a specific scenario by name */
|
|
533
|
+
run: (scenarioName?: string) => Promise<void>;
|
|
534
|
+
/** Run all scenarios in sequence */
|
|
535
|
+
runAll: () => Promise<void>;
|
|
536
|
+
/** Add a new scenario */
|
|
537
|
+
addScenario: (scenario: FeatureFlagScenario<FeatureFlagKey>) => void;
|
|
538
|
+
/** Remove a scenario by name */
|
|
539
|
+
removeScenario: (name: string) => void;
|
|
540
|
+
/** Clear all scenarios */
|
|
541
|
+
clear: () => void;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Custom assertion methods for feature flag testing
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* ```typescript
|
|
548
|
+
* // Custom matchers extend expect
|
|
549
|
+
* expect(featureFlag).toBeEnabled();
|
|
550
|
+
* expect(featureFlag).toHaveValue(true);
|
|
551
|
+
* expect(featureFlag).toHaveRolloutPercentage(25);
|
|
552
|
+
* expect(featureFlag).toBeEvaluatedAs(true, userContext);
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
export interface FeatureFlagAssertion {
|
|
556
|
+
/** Assert flag is enabled */
|
|
557
|
+
toBeEnabled: () => void;
|
|
558
|
+
/** Assert flag is disabled */
|
|
559
|
+
toBeDisabled: () => void;
|
|
560
|
+
/** Assert flag has specific value */
|
|
561
|
+
toHaveValue: (value: FeatureFlagValue) => void;
|
|
562
|
+
/** Assert flag has specific rollout percentage */
|
|
563
|
+
toHaveRolloutPercentage: (percentage: number) => void;
|
|
564
|
+
/** Assert flag has specific conditions */
|
|
565
|
+
toHaveConditions: (conditions: FeatureFlagCondition[]) => void;
|
|
566
|
+
/** Assert flag matches a condition */
|
|
567
|
+
toMatchCondition: (condition: FeatureFlagCondition) => void;
|
|
568
|
+
/** Assert flag evaluates to specific value in context */
|
|
569
|
+
toBeEvaluatedAs: (value: FeatureFlagValue, context?: FeatureFlagContext) => void;
|
|
570
|
+
/** Assert flag is targeted to specific context */
|
|
571
|
+
toBeTargetedTo: (context: FeatureFlagContext) => void;
|
|
572
|
+
/** Assert flag is rolled out to percentage */
|
|
573
|
+
toBeRolledOutTo: (percentage: number) => void;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Helper utilities for feature flag testing operations
|
|
577
|
+
*
|
|
578
|
+
* @example
|
|
579
|
+
* ```typescript
|
|
580
|
+
* const helpers: FeatureFlagHelpers = createFeatureFlagHelpers();
|
|
581
|
+
*
|
|
582
|
+
* // Create mocks
|
|
583
|
+
* const mockProvider = helpers.createProvider({ cache: true });
|
|
584
|
+
* const mockRepository = helpers.createRepository();
|
|
585
|
+
*
|
|
586
|
+
* // Setup scenario
|
|
587
|
+
* await helpers.setupScenario(testScenario);
|
|
588
|
+
*
|
|
589
|
+
* // Evaluate flags
|
|
590
|
+
* const evaluation = helpers.evaluateFlag(flag, context);
|
|
591
|
+
* const isEnabled = helpers.evaluateCondition(condition, context);
|
|
592
|
+
*
|
|
593
|
+
* // Generate test data
|
|
594
|
+
* const testContext = helpers.generateContext({ userId: 'test' });
|
|
595
|
+
* const testFlags = helpers.generateFlags(10);
|
|
596
|
+
* ```
|
|
597
|
+
*/
|
|
598
|
+
export interface FeatureFlagHelpersMock<FeatureFlagKey extends string> {
|
|
599
|
+
/** Create mock provider */
|
|
600
|
+
createProvider: (options?: FeatureFlagTestOptions<FeatureFlagKey>) => MockFeatureFlagProvider;
|
|
601
|
+
/** Create mock repository */
|
|
602
|
+
createRepository: () => MockFeatureFlagRepository;
|
|
603
|
+
/** Create mock service */
|
|
604
|
+
createService: () => MockFeatureFlagService;
|
|
605
|
+
/** Create mock engine */
|
|
606
|
+
createEngine: () => MockFeatureFlagEngine;
|
|
607
|
+
/** Create mock cache */
|
|
608
|
+
createCache: () => MockFeatureFlagCache;
|
|
609
|
+
/** Setup test scenario */
|
|
610
|
+
setupScenario: (scenario: FeatureFlagScenario<FeatureFlagKey>) => Promise<void>;
|
|
611
|
+
/** Evaluate flag manually */
|
|
612
|
+
evaluateFlag: (flag: FeatureFlag<FeatureFlagKey>, context?: FeatureFlagContext) => FeatureFlagEvaluation<FeatureFlagKey>;
|
|
613
|
+
/** Evaluate condition manually */
|
|
614
|
+
evaluateCondition: (condition: FeatureFlagCondition, context: FeatureFlagContext) => boolean;
|
|
615
|
+
/** Calculate rollout eligibility */
|
|
616
|
+
calculateRollout: (percentage: number, context: FeatureFlagContext) => boolean;
|
|
617
|
+
/** Generate test context */
|
|
618
|
+
generateContext: (overrides?: Partial<FeatureFlagContext>) => FeatureFlagContext;
|
|
619
|
+
/** Generate test flags */
|
|
620
|
+
generateFlags: (count: number, overrides?: Partial<FeatureFlag<FeatureFlagKey>>) => FeatureFlag<FeatureFlagKey>[];
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Configuration for feature flag testing environment
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* ```typescript
|
|
627
|
+
* const config: FeatureFlagTestConfig = {
|
|
628
|
+
* defaultProvider: 'memory',
|
|
629
|
+
* defaultTimeout: 5000,
|
|
630
|
+
* enableCache: false,
|
|
631
|
+
* cacheTimeout: 300000,
|
|
632
|
+
* enableLogging: true,
|
|
633
|
+
* logLevel: 'warn',
|
|
634
|
+
* mockOptions: {
|
|
635
|
+
* autoResolve: true,
|
|
636
|
+
* defaultEnabled: false,
|
|
637
|
+
* defaultValue: false,
|
|
638
|
+
* rolloutStrategy: 'deterministic'
|
|
639
|
+
* }
|
|
640
|
+
* };
|
|
641
|
+
*
|
|
642
|
+
* setupFeatureFlagTesting(config);
|
|
643
|
+
* ```
|
|
644
|
+
*/
|
|
645
|
+
export interface FeatureFlagTestConfig {
|
|
646
|
+
/** Default provider type */
|
|
647
|
+
defaultProvider: 'memory' | 'file' | 'database' | 'api' | 'redis';
|
|
648
|
+
/** Default request timeout */
|
|
649
|
+
defaultTimeout: number;
|
|
650
|
+
/** Enable caching */
|
|
651
|
+
enableCache: boolean;
|
|
652
|
+
/** Cache timeout in milliseconds */
|
|
653
|
+
cacheTimeout: number;
|
|
654
|
+
/** Enable logging */
|
|
655
|
+
enableLogging: boolean;
|
|
656
|
+
/** Log level */
|
|
657
|
+
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
|
658
|
+
/** Mock behavior options */
|
|
659
|
+
mockOptions: {
|
|
660
|
+
/** Auto-resolve promises */
|
|
661
|
+
autoResolve: boolean;
|
|
662
|
+
/** Default enabled state */
|
|
663
|
+
defaultEnabled: boolean;
|
|
664
|
+
/** Default flag value */
|
|
665
|
+
defaultValue: FeatureFlagValue;
|
|
666
|
+
/** Rollout calculation strategy */
|
|
667
|
+
rolloutStrategy: 'random' | 'deterministic';
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Feature flag event for tracking flag operations and changes
|
|
672
|
+
*
|
|
673
|
+
* @example
|
|
674
|
+
* ```typescript
|
|
675
|
+
* const event: FeatureFlagEvent = {
|
|
676
|
+
* type: 'evaluation',
|
|
677
|
+
* flagKey: 'new-checkout',
|
|
678
|
+
* context: { userId: 'user-123' },
|
|
679
|
+
* value: true,
|
|
680
|
+
* timestamp: new Date(),
|
|
681
|
+
* metadata: { source: 'api', version: '1.0' }
|
|
682
|
+
* };
|
|
683
|
+
*
|
|
684
|
+
* // Emit event
|
|
685
|
+
* eventEmitter.emit(event);
|
|
686
|
+
* ```
|
|
687
|
+
*/
|
|
688
|
+
export interface FeatureFlagEvent {
|
|
689
|
+
/** Type of flag operation */
|
|
690
|
+
type: 'evaluation' | 'update' | 'create' | 'delete' | 'refresh';
|
|
691
|
+
/** Flag identifier */
|
|
692
|
+
flagKey: string;
|
|
693
|
+
/** Evaluation context (for evaluations) */
|
|
694
|
+
context?: FeatureFlagContext;
|
|
695
|
+
/** Flag value (for evaluations/updates) */
|
|
696
|
+
value?: FeatureFlagValue;
|
|
697
|
+
/** Event timestamp */
|
|
698
|
+
timestamp: Date;
|
|
699
|
+
/** Additional event metadata */
|
|
700
|
+
metadata?: Record<string, unknown>;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Handler function for feature flag events
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* ```typescript
|
|
707
|
+
* const handler: FeatureFlagEventHandler = async (event) => {
|
|
708
|
+
* console.log(`Flag ${event.flagKey} was ${event.type}`);
|
|
709
|
+
*
|
|
710
|
+
* if (event.type === 'evaluation') {
|
|
711
|
+
* await logFlagUsage(event.flagKey, event.context, event.value);
|
|
712
|
+
* }
|
|
713
|
+
* };
|
|
714
|
+
*
|
|
715
|
+
* eventEmitter.on('evaluation', handler);
|
|
716
|
+
* ```
|
|
717
|
+
*/
|
|
718
|
+
export interface FeatureFlagEventHandler {
|
|
719
|
+
(event: FeatureFlagEvent): void | Promise<void>;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Event emitter for feature flag events with subscription management
|
|
723
|
+
*
|
|
724
|
+
* @example
|
|
725
|
+
* ```typescript
|
|
726
|
+
* const emitter: FeatureFlagEventEmitter = createFeatureFlagEventEmitter();
|
|
727
|
+
*
|
|
728
|
+
* // Subscribe to events
|
|
729
|
+
* const unsubscribe = emitter.on('evaluation', (event) => {
|
|
730
|
+
* console.log(`Flag ${event.flagKey} evaluated to ${event.value}`);
|
|
731
|
+
* });
|
|
732
|
+
*
|
|
733
|
+
* // One-time subscription
|
|
734
|
+
* emitter.once('update', (event) => {
|
|
735
|
+
* console.log('Flag updated:', event.flagKey);
|
|
736
|
+
* });
|
|
737
|
+
*
|
|
738
|
+
* // Emit events
|
|
739
|
+
* emitter.emit({
|
|
740
|
+
* type: 'evaluation',
|
|
741
|
+
* flagKey: 'my-flag',
|
|
742
|
+
* value: true,
|
|
743
|
+
* timestamp: new Date()
|
|
744
|
+
* });
|
|
745
|
+
*
|
|
746
|
+
* // Clean up
|
|
747
|
+
* unsubscribe();
|
|
748
|
+
* emitter.removeAllListeners();
|
|
749
|
+
* ```
|
|
750
|
+
*/
|
|
751
|
+
export interface FeatureFlagEventEmitter {
|
|
752
|
+
/** Subscribe to events */
|
|
753
|
+
on: (type: FeatureFlagEvent['type'], handler: FeatureFlagEventHandler) => () => void;
|
|
754
|
+
/** Subscribe to single event */
|
|
755
|
+
once: (type: FeatureFlagEvent['type'], handler: FeatureFlagEventHandler) => () => void;
|
|
756
|
+
/** Emit an event */
|
|
757
|
+
emit: (event: FeatureFlagEvent) => void;
|
|
758
|
+
/** Unsubscribe from events */
|
|
759
|
+
off: (type: FeatureFlagEvent['type'], handler: FeatureFlagEventHandler) => void;
|
|
760
|
+
/** Remove all listeners */
|
|
761
|
+
removeAllListeners: (type?: FeatureFlagEvent['type']) => void;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Custom assertions for React hook testing with feature flags
|
|
765
|
+
*
|
|
766
|
+
* @example
|
|
767
|
+
* ```typescript
|
|
768
|
+
* const { result } = renderHook(() => useFeatureFlag('my-flag'), {
|
|
769
|
+
* wrapper: FeatureFlagProvider
|
|
770
|
+
* });
|
|
771
|
+
*
|
|
772
|
+
* // Custom hook assertions
|
|
773
|
+
* expect(result).toBeEnabled('my-flag');
|
|
774
|
+
* expect(result).toBeDisabled('old-flag');
|
|
775
|
+
* expect(result).toHaveValue('theme-flag', 'dark');
|
|
776
|
+
* ```
|
|
777
|
+
*/
|
|
778
|
+
export interface FeatureFlagHookAssertion<FeatureFlagKey extends string> {
|
|
779
|
+
/** Assert hook shows flag as enabled */
|
|
780
|
+
toBeEnabled: (flagKey: FeatureFlagKey) => void;
|
|
781
|
+
/** Assert hook shows flag as disabled */
|
|
782
|
+
toBeDisabled: (flagKey: FeatureFlagKey) => void;
|
|
783
|
+
/** Assert hook shows flag with specific value */
|
|
784
|
+
toHaveValue: (flagKey: FeatureFlagKey, value: FeatureFlagValue) => void;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Stub definition for temporarily overriding feature flags in tests
|
|
788
|
+
*
|
|
789
|
+
* @example
|
|
790
|
+
* ```typescript
|
|
791
|
+
* const stub: FeatureFlagStub = {
|
|
792
|
+
* key: 'premium-feature',
|
|
793
|
+
* value: true,
|
|
794
|
+
* enabled: true,
|
|
795
|
+
* conditions: [{ field: 'tier', operator: 'equals', value: 'premium' }],
|
|
796
|
+
* rolloutPercentage: 100,
|
|
797
|
+
* temporary: true,
|
|
798
|
+
* priority: 10
|
|
799
|
+
* };
|
|
800
|
+
*
|
|
801
|
+
* stubManager.add(stub, { duration: 5000 });
|
|
802
|
+
* ```
|
|
803
|
+
*/
|
|
804
|
+
export interface FeatureFlagStub {
|
|
805
|
+
/** Flag key to stub */
|
|
806
|
+
key: string;
|
|
807
|
+
/** Value to return */
|
|
808
|
+
value: FeatureFlagValue;
|
|
809
|
+
/** Whether flag is enabled */
|
|
810
|
+
enabled: boolean;
|
|
811
|
+
/** Conditions for evaluation */
|
|
812
|
+
conditions?: FeatureFlagCondition[];
|
|
813
|
+
/** Rollout percentage override */
|
|
814
|
+
rolloutPercentage?: number;
|
|
815
|
+
/** Auto-remove after test */
|
|
816
|
+
temporary?: boolean;
|
|
817
|
+
/** Stub priority (higher wins) */
|
|
818
|
+
priority?: number;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Options for configuring feature flag stubs
|
|
822
|
+
*
|
|
823
|
+
* @example
|
|
824
|
+
* ```typescript
|
|
825
|
+
* const options: FeatureFlagStubOptions = {
|
|
826
|
+
* duration: 10000, // Auto-remove after 10 seconds
|
|
827
|
+
* priority: 5,
|
|
828
|
+
* overrideExisting: true,
|
|
829
|
+
* applyToAllTests: false,
|
|
830
|
+
* scoped: true // Only apply to current test
|
|
831
|
+
* };
|
|
832
|
+
*
|
|
833
|
+
* stubManager.add(myStub, options);
|
|
834
|
+
* ```
|
|
835
|
+
*/
|
|
836
|
+
export interface FeatureFlagStubOptions {
|
|
837
|
+
/** Auto-remove duration in milliseconds */
|
|
838
|
+
duration?: number;
|
|
839
|
+
/** Stub priority for conflicts */
|
|
840
|
+
priority?: number;
|
|
841
|
+
/** Override existing stubs */
|
|
842
|
+
overrideExisting?: boolean;
|
|
843
|
+
/** Apply to all tests in suite */
|
|
844
|
+
applyToAllTests?: boolean;
|
|
845
|
+
/** Scope to current test only */
|
|
846
|
+
scoped?: boolean;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Manager for feature flag stubs with full lifecycle management
|
|
850
|
+
*
|
|
851
|
+
* @example
|
|
852
|
+
* ```typescript
|
|
853
|
+
* const manager: FeatureFlagStubManager = createStubManager();
|
|
854
|
+
*
|
|
855
|
+
* // Add stub
|
|
856
|
+
* manager.add({
|
|
857
|
+
* key: 'test-feature',
|
|
858
|
+
* value: true,
|
|
859
|
+
* enabled: true
|
|
860
|
+
* });
|
|
861
|
+
*
|
|
862
|
+
* // Quick modifications
|
|
863
|
+
* manager.enable('disabled-feature');
|
|
864
|
+
* manager.setValue('config-flag', { theme: 'dark' });
|
|
865
|
+
* manager.setRollout('gradual-feature', 25);
|
|
866
|
+
*
|
|
867
|
+
* // Condition management
|
|
868
|
+
* manager.addCondition('targeted-feature', {
|
|
869
|
+
* field: 'userId',
|
|
870
|
+
* operator: 'in',
|
|
871
|
+
* value: ['user1', 'user2']
|
|
872
|
+
* });
|
|
873
|
+
*
|
|
874
|
+
* // Cleanup
|
|
875
|
+
* manager.clear();
|
|
876
|
+
* ```
|
|
877
|
+
*/
|
|
878
|
+
export interface FeatureFlagStubManager {
|
|
879
|
+
/** Add a new stub */
|
|
880
|
+
add: (stub: FeatureFlagStub, options?: FeatureFlagStubOptions) => void;
|
|
881
|
+
/** Remove stub by key */
|
|
882
|
+
remove: (key: string) => void;
|
|
883
|
+
/** Update existing stub */
|
|
884
|
+
update: (key: string, updates: Partial<FeatureFlagStub>) => void;
|
|
885
|
+
/** Clear all stubs */
|
|
886
|
+
clear: () => void;
|
|
887
|
+
/** List all stubs */
|
|
888
|
+
list: () => FeatureFlagStub[];
|
|
889
|
+
/** Get stub by key */
|
|
890
|
+
get: (key: string) => FeatureFlagStub | undefined;
|
|
891
|
+
/** Check if stub exists */
|
|
892
|
+
exists: (key: string) => boolean;
|
|
893
|
+
/** Enable flag stub */
|
|
894
|
+
enable: (key: string) => void;
|
|
895
|
+
/** Disable flag stub */
|
|
896
|
+
disable: (key: string) => void;
|
|
897
|
+
/** Set flag value */
|
|
898
|
+
setValue: (key: string, value: FeatureFlagValue) => void;
|
|
899
|
+
/** Set rollout percentage */
|
|
900
|
+
setRollout: (key: string, percentage: number) => void;
|
|
901
|
+
/** Add condition to flag */
|
|
902
|
+
addCondition: (key: string, condition: FeatureFlagCondition) => void;
|
|
903
|
+
/** Remove condition by index */
|
|
904
|
+
removeCondition: (key: string, index: number) => void;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Feature flag update operation with timing control
|
|
908
|
+
*
|
|
909
|
+
* @example
|
|
910
|
+
* ```typescript
|
|
911
|
+
* const update: FeatureFlagUpdate = {
|
|
912
|
+
* key: 'gradual-rollout',
|
|
913
|
+
* value: true,
|
|
914
|
+
* delay: 2000 // Apply after 2 seconds
|
|
915
|
+
* };
|
|
916
|
+
*
|
|
917
|
+
* // Schedule update
|
|
918
|
+
* await scheduleFeatureFlagUpdate(update);
|
|
919
|
+
* ```
|
|
920
|
+
*/
|
|
921
|
+
export interface FeatureFlagUpdate<FeatureFlagKey extends string> {
|
|
922
|
+
/** Flag key to update */
|
|
923
|
+
key: FeatureFlagKey;
|
|
924
|
+
/** New flag value */
|
|
925
|
+
value: FeatureFlagValue;
|
|
926
|
+
/** Delay before applying update (ms) */
|
|
927
|
+
delay: number;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Result of feature flag operation
|
|
931
|
+
*
|
|
932
|
+
* @example
|
|
933
|
+
* ```typescript
|
|
934
|
+
* const result: FeatureFlagResult = {
|
|
935
|
+
* success: true,
|
|
936
|
+
* value: { theme: 'dark', language: 'en' }
|
|
937
|
+
* };
|
|
938
|
+
*
|
|
939
|
+
* if (result.success) {
|
|
940
|
+
* console.log('Flag value:', result.value);
|
|
941
|
+
* }
|
|
942
|
+
* ```
|
|
943
|
+
*/
|
|
944
|
+
export interface FeatureFlagResult {
|
|
945
|
+
/** Whether operation succeeded */
|
|
946
|
+
success?: boolean;
|
|
947
|
+
/** Operation result value */
|
|
948
|
+
value?: unknown;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Result wrapper for feature flag hook values
|
|
952
|
+
*
|
|
953
|
+
* @typeParam T - Type of the hook result value
|
|
954
|
+
*
|
|
955
|
+
* @example
|
|
956
|
+
* ```typescript
|
|
957
|
+
* const hookResult: FeatureFlagHookResult<boolean> = {
|
|
958
|
+
* current: true
|
|
959
|
+
* };
|
|
960
|
+
*
|
|
961
|
+
* // Usage in hook
|
|
962
|
+
* const { result } = renderHook(() => useFeatureFlag('my-flag'));
|
|
963
|
+
* expect(result.current).toBe(true);
|
|
964
|
+
* ```
|
|
965
|
+
*/
|
|
966
|
+
export interface FeatureFlagHookResult<T> {
|
|
967
|
+
/** Current hook value */
|
|
968
|
+
current: T;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Props for feature flag test wrapper component
|
|
972
|
+
*
|
|
973
|
+
* @example
|
|
974
|
+
* ```typescript
|
|
975
|
+
* const WrapperComponent: React.FC<FeatureFlagWrapperProps> = ({ children }) => (
|
|
976
|
+
* <FeatureFlagProvider provider={mockProvider}>
|
|
977
|
+
* {children}
|
|
978
|
+
* </FeatureFlagProvider>
|
|
979
|
+
* );
|
|
980
|
+
*
|
|
981
|
+
* render(<MyComponent />, { wrapper: WrapperComponent });
|
|
982
|
+
* ```
|
|
983
|
+
*/
|
|
984
|
+
export interface FeatureFlagWrapperProps {
|
|
985
|
+
/** Child components to wrap */
|
|
986
|
+
children: React.ReactNode;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Feature flag operation descriptor for batch operations
|
|
990
|
+
*
|
|
991
|
+
* @example
|
|
992
|
+
* ```typescript
|
|
993
|
+
* const operations: FeatureFlagOperation[] = [
|
|
994
|
+
* { type: 'set', key: 'feature-a', value: true },
|
|
995
|
+
* { type: 'set', key: 'feature-b', value: 'variant-1' },
|
|
996
|
+
* { type: 'remove', key: 'old-feature' },
|
|
997
|
+
* { type: 'get', key: 'current-feature' }
|
|
998
|
+
* ];
|
|
999
|
+
*
|
|
1000
|
+
* const results = await batchFeatureFlagOperations(operations);
|
|
1001
|
+
* ```
|
|
1002
|
+
*/
|
|
1003
|
+
export interface FeatureFlagOperation<FeatureFlagKey extends string> {
|
|
1004
|
+
/** Operation type */
|
|
1005
|
+
type: 'set' | 'remove' | 'get';
|
|
1006
|
+
/** Flag key */
|
|
1007
|
+
key: FeatureFlagKey;
|
|
1008
|
+
/** Value for set operations */
|
|
1009
|
+
value?: FeatureFlagValue;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Performance metrics for feature flag operations
|
|
1013
|
+
*
|
|
1014
|
+
* @example
|
|
1015
|
+
* ```typescript
|
|
1016
|
+
* const metrics: FeatureFlagPerformanceMetrics = {
|
|
1017
|
+
* avg: 15.5, // Average response time in ms
|
|
1018
|
+
* min: 8.2, // Fastest response
|
|
1019
|
+
* max: 45.1, // Slowest response
|
|
1020
|
+
* total: 1000 // Total operations measured
|
|
1021
|
+
* };
|
|
1022
|
+
*
|
|
1023
|
+
* console.log(`Average evaluation time: ${metrics.avg}ms`);
|
|
1024
|
+
* ```
|
|
1025
|
+
*/
|
|
1026
|
+
export interface FeatureFlagPerformanceMetrics {
|
|
1027
|
+
/** Average operation time (ms) */
|
|
1028
|
+
avg: number;
|
|
1029
|
+
/** Minimum operation time (ms) */
|
|
1030
|
+
min: number;
|
|
1031
|
+
/** Maximum operation time (ms) */
|
|
1032
|
+
max: number;
|
|
1033
|
+
/** Total operations measured */
|
|
1034
|
+
total: number;
|
|
1035
|
+
}
|
|
1036
|
+
export interface FeatureFlagLoadTestResult {
|
|
1037
|
+
duration: number;
|
|
1038
|
+
errors: Error[];
|
|
1039
|
+
}
|
|
1040
|
+
export interface ControllableProvider<FeatureFlagKey extends string> {
|
|
1041
|
+
provider: IFeatureFlagProvider<FeatureFlagKey>;
|
|
1042
|
+
control: {
|
|
1043
|
+
resolveMethod: (method: string) => void;
|
|
1044
|
+
rejectMethod: (method: string, error: Error) => void;
|
|
1045
|
+
resolveAll: () => void;
|
|
1046
|
+
rejectAll: (error: Error) => void;
|
|
1047
|
+
reset: () => void;
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
export interface SubscriptionFeatureFlagTracker<FeatureFlagKey extends string> {
|
|
1051
|
+
track: (provider: IFeatureFlagProvider<FeatureFlagKey>, id?: string) => {
|
|
1052
|
+
id: string;
|
|
1053
|
+
callback: Vitest.Mock;
|
|
1054
|
+
unsubscribe: () => void;
|
|
1055
|
+
};
|
|
1056
|
+
unsubscribe: (id: string) => void;
|
|
1057
|
+
unsubscribeAll: () => void;
|
|
1058
|
+
getCallCount: (id: string) => number;
|
|
1059
|
+
getLastCall: (id: string) => unknown;
|
|
1060
|
+
reset: () => void;
|
|
1061
|
+
}
|
|
1062
|
+
export interface EvaluationAssertions {
|
|
1063
|
+
hasValue: (expected: unknown) => void;
|
|
1064
|
+
hasReason: (expected: string) => void;
|
|
1065
|
+
hasSource: (expected: string) => void;
|
|
1066
|
+
hasRuleId: (expected: string) => void;
|
|
1067
|
+
wasEvaluatedRecently: (maxAgeMs?: number) => void;
|
|
1068
|
+
}
|
|
1069
|
+
export interface TestFeatureFlagProviderProps<FeatureFlagKey extends string> {
|
|
1070
|
+
provider: IFeatureFlagProvider<FeatureFlagKey>;
|
|
1071
|
+
children: React.ReactNode;
|
|
1072
|
+
context?: React.Context<FeatureFlagContextValue<FeatureFlagKey> | null>;
|
|
1073
|
+
}
|
|
1074
|
+
export interface CreateProviderForTestOptions<FeatureFlagKey extends string> {
|
|
1075
|
+
provider?: IFeatureFlagProvider<FeatureFlagKey>;
|
|
1076
|
+
config?: Partial<FeatureFlagConfig<FeatureFlagKey>>;
|
|
1077
|
+
flags?: Record<string, FeatureFlagValue>;
|
|
1078
|
+
features?: Record<FeatureFlagKey, FeatureFlagValue>;
|
|
1079
|
+
ProviderClass?: new (config: FeatureFlagConfig<FeatureFlagKey>, features: Record<FeatureFlagKey, FeatureFlagValue>) => IFeatureFlagProvider<FeatureFlagKey>;
|
|
1080
|
+
}
|
|
1081
|
+
export interface SetupFeatureFlagTestOptions<FeatureFlagKey extends string> {
|
|
1082
|
+
provider?: IFeatureFlagProvider<FeatureFlagKey>;
|
|
1083
|
+
config?: Partial<FeatureFlagConfig<FeatureFlagKey>>;
|
|
1084
|
+
flags?: Record<string, FeatureFlagValue>;
|
|
1085
|
+
setupBeforeEach?: boolean;
|
|
1086
|
+
autoInit: boolean;
|
|
1087
|
+
context?: React.Context<FeatureFlagContextValue<FeatureFlagKey> | null>;
|
|
1088
|
+
ProviderClass?: new (config: FeatureFlagConfig<FeatureFlagKey>, features: Record<FeatureFlagKey, FeatureFlagValue>) => IFeatureFlagProvider<FeatureFlagKey>;
|
|
1089
|
+
features?: Record<FeatureFlagKey, FeatureFlagValue>;
|
|
1090
|
+
}
|
|
1091
|
+
export interface PercentageRolloutScenario<FeatureFlagKey extends string> {
|
|
1092
|
+
rule: FeatureFlagRule<FeatureFlagKey>;
|
|
1093
|
+
users: string[];
|
|
1094
|
+
expectedEnabled: number;
|
|
1095
|
+
evaluate: (userId: string) => boolean;
|
|
1096
|
+
}
|
|
1097
|
+
export interface TimeBasedRolloutScenario<FeatureFlagKey extends string> {
|
|
1098
|
+
rule: FeatureFlagRule<FeatureFlagKey>;
|
|
1099
|
+
isActive: (date?: Date) => boolean;
|
|
1100
|
+
}
|
|
1101
|
+
export interface TargetedUsersScenario<FeatureFlagKey extends string> {
|
|
1102
|
+
rule: FeatureFlagRule<FeatureFlagKey>;
|
|
1103
|
+
isTargeted: (userId: string) => boolean;
|
|
1104
|
+
}
|
|
1105
|
+
export interface ABTestScenario<FeatureFlagKey extends string> {
|
|
1106
|
+
rules: FeatureFlagRule<FeatureFlagKey>[];
|
|
1107
|
+
variants: Record<string, FeatureFlagValue>;
|
|
1108
|
+
distribution: Record<string, number>;
|
|
1109
|
+
assignVariant: (userId: string) => string;
|
|
1110
|
+
}
|
|
1111
|
+
export interface ComplexRulesScenario<FeatureFlagKey extends string> {
|
|
1112
|
+
rules: FeatureFlagRule<FeatureFlagKey>[];
|
|
1113
|
+
evaluate: (context: FeatureFlagContext) => FeatureFlagEvaluation<FeatureFlagKey>;
|
|
1114
|
+
}
|
|
1115
|
+
export interface MockFeatureFlagProviderWithState<FeatureFlagKey extends string> extends IFeatureFlagProvider<FeatureFlagKey> {
|
|
1116
|
+
_mockState: {
|
|
1117
|
+
flags: Map<FeatureFlagKey, FeatureFlag<FeatureFlagKey>>;
|
|
1118
|
+
overrides: Map<FeatureFlagKey, FeatureFlagValue>;
|
|
1119
|
+
subscribers: Set<(flags: FeatureFlag<FeatureFlagKey>[]) => void>;
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
export interface TestFeatureFlagContextValue<FeatureFlagKey extends string> {
|
|
1123
|
+
provider: IFeatureFlagProvider<FeatureFlagKey>;
|
|
1124
|
+
isInitialized: boolean;
|
|
1125
|
+
isLoading: boolean;
|
|
1126
|
+
error: Error | null;
|
|
1127
|
+
lastUpdated: Date;
|
|
1128
|
+
refresh: () => Promise<void>;
|
|
1129
|
+
}
|
|
1130
|
+
export interface WaitForFlagValueOptions {
|
|
1131
|
+
context?: FeatureFlagContext;
|
|
1132
|
+
timeout?: number;
|
|
1133
|
+
}
|